mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution][Exceptions] - Common flyout components (#142054)
## Summary Adds components shared between new add/edit exception flyouts. Does not yet modify the flyouts themselves. Trying to break down what would be an even larger PR into chunks.
This commit is contained in:
parent
63aee48127
commit
0149bd063c
59 changed files with 4206 additions and 529 deletions
|
@ -138,13 +138,13 @@ export const getNewExceptionItem = ({
|
|||
namespaceType,
|
||||
ruleName,
|
||||
}: {
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
listId: string | undefined;
|
||||
namespaceType: NamespaceType | undefined;
|
||||
ruleName: string;
|
||||
}): CreateExceptionListItemBuilderSchema => {
|
||||
return {
|
||||
comments: [],
|
||||
description: `${ruleName} - exception list item`,
|
||||
description: 'Exception list item',
|
||||
entries: addIdToEntries([
|
||||
{
|
||||
field: '',
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { DataViewFieldBase } from '@kbn/es-query';
|
||||
import type {
|
||||
CreateExceptionListItemSchema,
|
||||
CreateRuleExceptionListItemSchema,
|
||||
Entry,
|
||||
EntryExists,
|
||||
EntryMatch,
|
||||
|
@ -18,6 +19,7 @@ import type {
|
|||
ExceptionListItemSchema,
|
||||
ListOperatorEnum as OperatorEnum,
|
||||
ListOperatorTypeEnum as OperatorTypeEnum,
|
||||
NamespaceType,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import {
|
||||
EXCEPTION_LIST_NAMESPACE,
|
||||
|
@ -93,16 +95,23 @@ export type ExceptionListItemBuilderSchema = Omit<ExceptionListItemSchema, 'entr
|
|||
|
||||
export type CreateExceptionListItemBuilderSchema = Omit<
|
||||
CreateExceptionListItemSchema,
|
||||
'meta' | 'entries'
|
||||
'meta' | 'entries' | 'list_id' | 'namespace_type'
|
||||
> & {
|
||||
meta: { temporaryUuid: string };
|
||||
entries: BuilderEntry[];
|
||||
list_id: string | undefined;
|
||||
namespace_type: NamespaceType | undefined;
|
||||
};
|
||||
|
||||
export type ExceptionsBuilderExceptionItem =
|
||||
| ExceptionListItemBuilderSchema
|
||||
| CreateExceptionListItemBuilderSchema;
|
||||
|
||||
export type ExceptionsBuilderReturnExceptionItem =
|
||||
| ExceptionListItemSchema
|
||||
| CreateExceptionListItemSchema
|
||||
| CreateRuleExceptionListItemSchema;
|
||||
|
||||
export const exceptionListSavedObjectType = EXCEPTION_LIST_NAMESPACE;
|
||||
export const exceptionListAgnosticSavedObjectType = EXCEPTION_LIST_NAMESPACE_AGNOSTIC;
|
||||
export type SavedObjectType =
|
||||
|
|
|
@ -11,7 +11,6 @@ import styled from 'styled-components';
|
|||
import { HttpStart } from '@kbn/core/public';
|
||||
import { addIdToItem } from '@kbn/securitysolution-utils';
|
||||
import {
|
||||
CreateExceptionListItemSchema,
|
||||
ExceptionListItemSchema,
|
||||
ExceptionListType,
|
||||
NamespaceType,
|
||||
|
@ -24,6 +23,7 @@ import {
|
|||
import {
|
||||
CreateExceptionListItemBuilderSchema,
|
||||
ExceptionsBuilderExceptionItem,
|
||||
ExceptionsBuilderReturnExceptionItem,
|
||||
OperatorOption,
|
||||
containsValueListEntry,
|
||||
filterExceptionItems,
|
||||
|
@ -68,7 +68,7 @@ const initialState: State = {
|
|||
|
||||
export interface OnChangeProps {
|
||||
errorExists: boolean;
|
||||
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>;
|
||||
exceptionItems: ExceptionsBuilderReturnExceptionItem[];
|
||||
exceptionsToDelete: ExceptionListItemSchema[];
|
||||
warningExists: boolean;
|
||||
}
|
||||
|
@ -84,15 +84,16 @@ export interface ExceptionBuilderProps {
|
|||
isNestedDisabled: boolean;
|
||||
isOrDisabled: boolean;
|
||||
isOrHidden?: boolean;
|
||||
listId: string;
|
||||
listNamespaceType: NamespaceType;
|
||||
listId: string | undefined;
|
||||
listNamespaceType: NamespaceType | undefined;
|
||||
listType: ExceptionListType;
|
||||
listTypeSpecificIndexPatternFilter?: (
|
||||
pattern: DataViewBase,
|
||||
type: ExceptionListType
|
||||
) => DataViewBase;
|
||||
onChange: (arg: OnChangeProps) => void;
|
||||
ruleName: string;
|
||||
exceptionItemName?: string;
|
||||
ruleName?: string;
|
||||
isDisabled?: boolean;
|
||||
operatorsList?: OperatorOption[];
|
||||
}
|
||||
|
@ -113,6 +114,7 @@ export const ExceptionBuilderComponent = ({
|
|||
listTypeSpecificIndexPatternFilter,
|
||||
onChange,
|
||||
ruleName,
|
||||
exceptionItemName,
|
||||
isDisabled = false,
|
||||
osTypes,
|
||||
operatorsList,
|
||||
|
@ -289,10 +291,10 @@ export const ExceptionBuilderComponent = ({
|
|||
const newException = getNewExceptionItem({
|
||||
listId,
|
||||
namespaceType: listNamespaceType,
|
||||
ruleName,
|
||||
ruleName: exceptionItemName ?? `${ruleName ?? 'Rule'} - Exception item`,
|
||||
});
|
||||
setUpdateExceptions([...exceptions, { ...newException }]);
|
||||
}, [setUpdateExceptions, exceptions, listId, listNamespaceType, ruleName]);
|
||||
}, [listId, listNamespaceType, exceptionItemName, ruleName, setUpdateExceptions, exceptions]);
|
||||
|
||||
// The builder can have existing exception items, or new exception items that have yet
|
||||
// to be created (and thus lack an id), this was creating some React bugs with relying
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('find_exception_list_references_schema', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('"ids" cannot be undefined', () => {
|
||||
test('"ids" can be optional', () => {
|
||||
const payload: Omit<FindExceptionReferencesOnRuleSchema, 'ids'> = {
|
||||
list_ids: '123,456',
|
||||
namespace_types: 'single,agnostic',
|
||||
|
@ -37,11 +37,14 @@ describe('find_exception_list_references_schema', () => {
|
|||
const decoded = findExceptionReferencesOnRuleSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const output = foldLeftRight(checked);
|
||||
expect(formatErrors(output.errors)).toEqual(['Invalid value "undefined" supplied to "ids"']);
|
||||
expect(output.schema).toEqual({});
|
||||
expect(formatErrors(output.errors)).toEqual([]);
|
||||
expect(output.schema).toEqual({
|
||||
list_ids: ['123', '456'],
|
||||
namespace_types: ['single', 'agnostic'],
|
||||
});
|
||||
});
|
||||
|
||||
test('"list_ids" cannot be undefined', () => {
|
||||
test('"list_ids" can be undefined', () => {
|
||||
const payload: Omit<FindExceptionReferencesOnRuleSchema, 'list_ids'> = {
|
||||
ids: 'abc',
|
||||
namespace_types: 'single',
|
||||
|
@ -50,10 +53,11 @@ describe('find_exception_list_references_schema', () => {
|
|||
const decoded = findExceptionReferencesOnRuleSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const output = foldLeftRight(checked);
|
||||
expect(formatErrors(output.errors)).toEqual([
|
||||
'Invalid value "undefined" supplied to "list_ids"',
|
||||
]);
|
||||
expect(output.schema).toEqual({});
|
||||
expect(formatErrors(output.errors)).toEqual([]);
|
||||
expect(output.schema).toEqual({
|
||||
ids: ['abc'],
|
||||
namespace_types: ['single'],
|
||||
});
|
||||
});
|
||||
|
||||
test('defaults "namespacetypes" to ["single"] if none set', () => {
|
||||
|
|
|
@ -9,13 +9,21 @@ import * as t from 'io-ts';
|
|||
import { NonEmptyStringArray } from '@kbn/securitysolution-io-ts-types';
|
||||
import { DefaultNamespaceArray } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
export const findExceptionReferencesOnRuleSchema = t.exact(
|
||||
t.type({
|
||||
ids: NonEmptyStringArray,
|
||||
list_ids: NonEmptyStringArray,
|
||||
namespace_types: DefaultNamespaceArray,
|
||||
})
|
||||
);
|
||||
// If ids and list_ids are undefined, route will fetch all lists matching the
|
||||
// specified namespace type
|
||||
export const findExceptionReferencesOnRuleSchema = t.intersection([
|
||||
t.exact(
|
||||
t.type({
|
||||
namespace_types: DefaultNamespaceArray,
|
||||
})
|
||||
),
|
||||
t.exact(
|
||||
t.partial({
|
||||
ids: NonEmptyStringArray,
|
||||
list_ids: NonEmptyStringArray,
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
||||
export type FindExceptionReferencesOnRuleSchema = t.OutputOf<
|
||||
typeof findExceptionReferencesOnRuleSchema
|
||||
|
|
|
@ -7,62 +7,66 @@
|
|||
|
||||
import { exactCheck, formatErrors, foldLeftRight } from '@kbn/securitysolution-io-ts-utils';
|
||||
import {
|
||||
ruleReferenceSchema,
|
||||
exceptionListRuleReferencesSchema,
|
||||
rulesReferencedByExceptionListsSchema,
|
||||
} from './find_exception_list_references_schema';
|
||||
import type {
|
||||
RuleReferenceSchema,
|
||||
ExceptionListRuleReferencesSchema,
|
||||
RulesReferencedByExceptionListsSchema,
|
||||
} from './find_exception_list_references_schema';
|
||||
import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock';
|
||||
|
||||
describe('find_exception_list_references_schema', () => {
|
||||
describe('ruleReferenceSchema', () => {
|
||||
describe('exceptionListRuleReferencesSchema', () => {
|
||||
test('validates all fields', () => {
|
||||
const payload: RuleReferenceSchema = {
|
||||
name: 'My rule',
|
||||
id: '4656dc92-5832-11ea-8e2d-0242ac130003',
|
||||
rule_id: 'my-rule-id',
|
||||
exception_lists: [
|
||||
const payload: ExceptionListRuleReferencesSchema = {
|
||||
...getExceptionListSchemaMock(),
|
||||
referenced_rules: [
|
||||
{
|
||||
id: 'myListId',
|
||||
list_id: 'my-list-id',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
name: 'My rule',
|
||||
id: '4656dc92-5832-11ea-8e2d-0242ac130003',
|
||||
rule_id: 'my-rule-id',
|
||||
exception_lists: [
|
||||
{
|
||||
id: 'myListId',
|
||||
list_id: 'my-list-id',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const decoded = ruleReferenceSchema.decode(payload);
|
||||
const decoded = exceptionListRuleReferencesSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const output = foldLeftRight(checked);
|
||||
expect(formatErrors(output.errors)).toEqual([]);
|
||||
expect(output.schema).toEqual({
|
||||
exception_lists: [
|
||||
{ id: 'myListId', list_id: 'my-list-id', namespace_type: 'single', type: 'detection' },
|
||||
],
|
||||
id: '4656dc92-5832-11ea-8e2d-0242ac130003',
|
||||
name: 'My rule',
|
||||
rule_id: 'my-rule-id',
|
||||
});
|
||||
expect(output.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('cannot add extra values', () => {
|
||||
const payload: RuleReferenceSchema & { extra_value?: string } = {
|
||||
name: 'My rule',
|
||||
id: '4656dc92-5832-11ea-8e2d-0242ac130003',
|
||||
rule_id: 'my-rule-id',
|
||||
const payload: ExceptionListRuleReferencesSchema & { extra_value?: string } = {
|
||||
extra_value: 'foo',
|
||||
exception_lists: [
|
||||
...getExceptionListSchemaMock(),
|
||||
referenced_rules: [
|
||||
{
|
||||
id: 'myListId',
|
||||
list_id: 'my-list-id',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
name: 'My rule',
|
||||
id: '4656dc92-5832-11ea-8e2d-0242ac130003',
|
||||
rule_id: 'my-rule-id',
|
||||
exception_lists: [
|
||||
{
|
||||
id: 'myListId',
|
||||
list_id: 'my-list-id',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const decoded = ruleReferenceSchema.decode(payload);
|
||||
const decoded = exceptionListRuleReferencesSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const output = foldLeftRight(checked);
|
||||
expect(formatErrors(output.errors)).toEqual(['invalid keys "extra_value"']);
|
||||
|
@ -75,21 +79,24 @@ describe('find_exception_list_references_schema', () => {
|
|||
const payload: RulesReferencedByExceptionListsSchema = {
|
||||
references: [
|
||||
{
|
||||
'my-list-id': [
|
||||
{
|
||||
name: 'My rule',
|
||||
id: '4656dc92-5832-11ea-8e2d-0242ac130003',
|
||||
rule_id: 'my-rule-id',
|
||||
exception_lists: [
|
||||
{
|
||||
id: 'myListId',
|
||||
list_id: 'my-list-id',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'my-list-id': {
|
||||
...getExceptionListSchemaMock(),
|
||||
referenced_rules: [
|
||||
{
|
||||
name: 'My rule',
|
||||
id: '4656dc92-5832-11ea-8e2d-0242ac130003',
|
||||
rule_id: 'my-rule-id',
|
||||
exception_lists: [
|
||||
{
|
||||
id: 'myListId',
|
||||
list_id: 'my-list-id',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -98,27 +105,7 @@ describe('find_exception_list_references_schema', () => {
|
|||
const checked = exactCheck(payload, decoded);
|
||||
const output = foldLeftRight(checked);
|
||||
expect(formatErrors(output.errors)).toEqual([]);
|
||||
expect(output.schema).toEqual({
|
||||
references: [
|
||||
{
|
||||
'my-list-id': [
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: 'myListId',
|
||||
list_id: 'my-list-id',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: '4656dc92-5832-11ea-8e2d-0242ac130003',
|
||||
name: 'My rule',
|
||||
rule_id: 'my-rule-id',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(output.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('validates "references" with empty array', () => {
|
||||
|
@ -140,21 +127,24 @@ describe('find_exception_list_references_schema', () => {
|
|||
extra_value: 'foo',
|
||||
references: [
|
||||
{
|
||||
'my-list-id': [
|
||||
{
|
||||
name: 'My rule',
|
||||
id: '4656dc92-5832-11ea-8e2d-0242ac130003',
|
||||
rule_id: 'my-rule-id',
|
||||
exception_lists: [
|
||||
{
|
||||
id: 'myListId',
|
||||
list_id: 'my-list-id',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'my-list-id': {
|
||||
...getExceptionListSchemaMock(),
|
||||
referenced_rules: [
|
||||
{
|
||||
name: 'My rule',
|
||||
id: '4656dc92-5832-11ea-8e2d-0242ac130003',
|
||||
rule_id: 'my-rule-id',
|
||||
exception_lists: [
|
||||
{
|
||||
id: 'myListId',
|
||||
list_id: 'my-list-id',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
import { listArray, list_id } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { exceptionListSchema, listArray, list_id } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { rule_id, id, name } from '../common/schemas';
|
||||
|
||||
export const ruleReferenceSchema = t.exact(
|
||||
export const ruleReferenceRuleInfoSchema = t.exact(
|
||||
t.type({
|
||||
name,
|
||||
id,
|
||||
|
@ -20,9 +20,25 @@ export const ruleReferenceSchema = t.exact(
|
|||
})
|
||||
);
|
||||
|
||||
export type RuleReferenceSchema = t.OutputOf<typeof ruleReferenceSchema>;
|
||||
export type ExceptionListRuleReferencesInfoSchema = t.OutputOf<typeof ruleReferenceRuleInfoSchema>;
|
||||
|
||||
export const rulesReferencedByExceptionListSchema = t.record(list_id, t.array(ruleReferenceSchema));
|
||||
export const exceptionListRuleReferencesSchema = t.intersection([
|
||||
exceptionListSchema,
|
||||
t.exact(
|
||||
t.type({
|
||||
referenced_rules: t.array(ruleReferenceRuleInfoSchema),
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
||||
export type ExceptionListRuleReferencesSchema = t.OutputOf<
|
||||
typeof exceptionListRuleReferencesSchema
|
||||
>;
|
||||
|
||||
export const rulesReferencedByExceptionListSchema = t.record(
|
||||
list_id,
|
||||
exceptionListRuleReferencesSchema
|
||||
);
|
||||
|
||||
export type RuleReferencesSchema = t.OutputOf<typeof rulesReferencedByExceptionListSchema>;
|
||||
|
||||
|
|
|
@ -30,12 +30,15 @@ import {
|
|||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import type {
|
||||
CreateExceptionListItemSchema,
|
||||
ExceptionListItemSchema,
|
||||
ExceptionListType,
|
||||
OsTypeArray,
|
||||
ExceptionListItemSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type { ExceptionsBuilderExceptionItem } from '@kbn/securitysolution-list-utils';
|
||||
import type {
|
||||
ExceptionsBuilderExceptionItem,
|
||||
ExceptionsBuilderReturnExceptionItem,
|
||||
} from '@kbn/securitysolution-list-utils';
|
||||
import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public';
|
||||
import type { DataViewBase } from '@kbn/es-query';
|
||||
import { useRuleIndices } from '../../../../detections/containers/detection_engine/rules/use_rule_indices';
|
||||
|
@ -146,7 +149,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
|
|||
const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false);
|
||||
const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false);
|
||||
const [exceptionItemsToAdd, setExceptionItemsToAdd] = useState<
|
||||
Array<ExceptionListItemSchema | CreateExceptionListItemSchema>
|
||||
ExceptionsBuilderReturnExceptionItem[]
|
||||
>([]);
|
||||
const [fetchOrCreateListError, setFetchOrCreateListError] = useState<ErrorInfo | null>(null);
|
||||
const { addError, addSuccess, addWarning } = useAppToasts();
|
||||
|
@ -190,7 +193,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
|
|||
exceptionItems,
|
||||
errorExists,
|
||||
}: {
|
||||
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>;
|
||||
exceptionItems: ExceptionsBuilderReturnExceptionItem[];
|
||||
errorExists: boolean;
|
||||
}) => {
|
||||
setExceptionItemsToAdd(exceptionItems);
|
||||
|
@ -337,10 +340,8 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
|
|||
return hasAlertData ? retrieveAlertOsTypes(alertData) : selectedOs ? [...selectedOs] : [];
|
||||
}, [hasAlertData, alertData, selectedOs]);
|
||||
|
||||
const enrichExceptionItems = useCallback((): Array<
|
||||
ExceptionListItemSchema | CreateExceptionListItemSchema
|
||||
> => {
|
||||
let enriched: Array<ExceptionListItemSchema | CreateExceptionListItemSchema> = [];
|
||||
const enrichExceptionItems = useCallback((): ExceptionsBuilderReturnExceptionItem[] => {
|
||||
let enriched: ExceptionsBuilderReturnExceptionItem[] = [];
|
||||
enriched =
|
||||
comment !== ''
|
||||
? enrichNewExceptionItemsWithComments(exceptionItemsToAdd, [{ comment }])
|
||||
|
@ -359,7 +360,9 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
|
|||
shouldBulkCloseAlert && signalIndexName != null ? [signalIndexName] : undefined;
|
||||
addOrUpdateExceptionItems(
|
||||
maybeRule?.rule_id ?? '',
|
||||
enrichExceptionItems(),
|
||||
// This is being rewritten in https://github.com/elastic/kibana/pull/140643
|
||||
// As of now, flyout cannot yet create item of type CreateRuleExceptionListItemSchema
|
||||
enrichExceptionItems() as Array<ExceptionListItemSchema | CreateExceptionListItemSchema>,
|
||||
alertIdToClose,
|
||||
bulkCloseIndex
|
||||
);
|
||||
|
|
|
@ -69,7 +69,9 @@ const ExceptionItemsViewerComponent: React.FC<ExceptionItemsViewerProps> = ({
|
|||
disableActions={disableActions}
|
||||
exceptionItem={exception}
|
||||
listType={listType}
|
||||
ruleReferences={ruleReferences != null ? ruleReferences[exception.list_id] : []}
|
||||
ruleReferences={
|
||||
ruleReferences != null ? ruleReferences[exception.list_id] : null
|
||||
}
|
||||
onDeleteException={onDeleteException}
|
||||
onEditException={onEditExceptionItem}
|
||||
dataTestSubj="exceptionItemsViewerItem"
|
||||
|
|
|
@ -162,8 +162,17 @@ const ExceptionsViewerComponent = ({
|
|||
[dispatch]
|
||||
);
|
||||
|
||||
const [isLoadingReferences, isFetchReferencesError, allReferences] =
|
||||
useFindExceptionListReferences(exceptionListsToQuery);
|
||||
const [isLoadingReferences, isFetchReferencesError, allReferences, fetchReferences] =
|
||||
useFindExceptionListReferences();
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchReferences != null && exceptionListsToQuery.length) {
|
||||
const listsToQuery = exceptionListsToQuery.map(
|
||||
({ id, list_id: listId, namespace_type: namespaceType }) => ({ id, listId, namespaceType })
|
||||
);
|
||||
fetchReferences(listsToQuery);
|
||||
}
|
||||
}, [exceptionListsToQuery, fetchReferences]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFetchReferencesError) {
|
||||
|
|
|
@ -38,6 +38,7 @@ import type {
|
|||
import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public';
|
||||
import type { DataViewBase } from '@kbn/es-query';
|
||||
|
||||
import type { ExceptionsBuilderReturnExceptionItem } from '@kbn/securitysolution-list-utils';
|
||||
import { useRuleIndices } from '../../../../detections/containers/detection_engine/rules/use_rule_indices';
|
||||
import { hasEqlSequenceQuery, isEqlRule } from '../../../../../common/detection_engine/utils';
|
||||
import { useFetchIndex } from '../../../../common/containers/source';
|
||||
|
@ -127,7 +128,7 @@ export const EditExceptionFlyout = memo(function EditExceptionFlyout({
|
|||
const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false);
|
||||
const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false);
|
||||
const [exceptionItemsToAdd, setExceptionItemsToAdd] = useState<
|
||||
Array<ExceptionListItemSchema | CreateExceptionListItemSchema>
|
||||
ExceptionsBuilderReturnExceptionItem[]
|
||||
>([]);
|
||||
const { addError, addSuccess } = useAppToasts();
|
||||
const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex();
|
||||
|
@ -254,7 +255,7 @@ export const EditExceptionFlyout = memo(function EditExceptionFlyout({
|
|||
exceptionItems,
|
||||
errorExists,
|
||||
}: {
|
||||
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>;
|
||||
exceptionItems: ExceptionsBuilderReturnExceptionItem[];
|
||||
errorExists: boolean;
|
||||
}) => {
|
||||
setExceptionItemsToAdd(exceptionItems);
|
||||
|
@ -279,7 +280,7 @@ export const EditExceptionFlyout = memo(function EditExceptionFlyout({
|
|||
|
||||
const enrichExceptionItems = useCallback(() => {
|
||||
const [exceptionItemToEdit] = exceptionItemsToAdd;
|
||||
let enriched: Array<ExceptionListItemSchema | CreateExceptionListItemSchema> = [
|
||||
let enriched: ExceptionsBuilderReturnExceptionItem[] = [
|
||||
{
|
||||
...enrichExistingExceptionItemWithComments(exceptionItemToEdit, [
|
||||
...exceptionItem.comments,
|
||||
|
@ -299,7 +300,9 @@ export const EditExceptionFlyout = memo(function EditExceptionFlyout({
|
|||
shouldBulkCloseAlert && signalIndexName !== null ? [signalIndexName] : undefined;
|
||||
addOrUpdateExceptionItems(
|
||||
maybeRule?.rule_id ?? '',
|
||||
enrichExceptionItems(),
|
||||
// This is being rewritten in https://github.com/elastic/kibana/pull/140643
|
||||
// As of now, flyout cannot yet create item of type CreateRuleExceptionListItemSchema
|
||||
enrichExceptionItems() as Array<ExceptionListItemSchema | CreateExceptionListItemSchema>,
|
||||
undefined,
|
||||
bulkCloseIndex
|
||||
);
|
||||
|
|
|
@ -13,6 +13,7 @@ import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas
|
|||
import { getCommentsArrayMock } from '@kbn/lists-plugin/common/schemas/types/comment.mock';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
|
@ -28,27 +29,30 @@ describe('ExceptionItemCard', () => {
|
|||
onEditException={jest.fn()}
|
||||
exceptionItem={exceptionItem}
|
||||
listType={ExceptionListTypeEnum.DETECTION}
|
||||
ruleReferences={[
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
},
|
||||
]}
|
||||
ruleReferences={{
|
||||
...getExceptionListSchemaMock(),
|
||||
referenced_rules: [
|
||||
{
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
dataTestSubj="item"
|
||||
/>
|
||||
</TestProviders>
|
||||
|
@ -74,27 +78,30 @@ describe('ExceptionItemCard', () => {
|
|||
exceptionItem={exceptionItem}
|
||||
dataTestSubj="item"
|
||||
listType={ExceptionListTypeEnum.DETECTION}
|
||||
ruleReferences={[
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
},
|
||||
]}
|
||||
ruleReferences={{
|
||||
...getExceptionListSchemaMock(),
|
||||
referenced_rules: [
|
||||
{
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -119,27 +126,30 @@ describe('ExceptionItemCard', () => {
|
|||
exceptionItem={exceptionItem}
|
||||
dataTestSubj="item"
|
||||
listType={ExceptionListTypeEnum.DETECTION}
|
||||
ruleReferences={[
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
},
|
||||
]}
|
||||
ruleReferences={{
|
||||
...getExceptionListSchemaMock(),
|
||||
referenced_rules: [
|
||||
{
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -160,27 +170,30 @@ describe('ExceptionItemCard', () => {
|
|||
exceptionItem={exceptionItem}
|
||||
dataTestSubj="item"
|
||||
listType={ExceptionListTypeEnum.DETECTION}
|
||||
ruleReferences={[
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
},
|
||||
]}
|
||||
ruleReferences={{
|
||||
...getExceptionListSchemaMock(),
|
||||
referenced_rules: [
|
||||
{
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -210,27 +223,30 @@ describe('ExceptionItemCard', () => {
|
|||
exceptionItem={exceptionItem}
|
||||
dataTestSubj="item"
|
||||
listType={ExceptionListTypeEnum.DETECTION}
|
||||
ruleReferences={[
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
},
|
||||
]}
|
||||
ruleReferences={{
|
||||
...getExceptionListSchemaMock(),
|
||||
referenced_rules: [
|
||||
{
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -263,27 +279,30 @@ describe('ExceptionItemCard', () => {
|
|||
exceptionItem={exceptionItem}
|
||||
dataTestSubj="item"
|
||||
listType={ExceptionListTypeEnum.DETECTION}
|
||||
ruleReferences={[
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
},
|
||||
]}
|
||||
ruleReferences={{
|
||||
...getExceptionListSchemaMock(),
|
||||
referenced_rules: [
|
||||
{
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
|
|
@ -17,14 +17,14 @@ import * as i18n from './translations';
|
|||
import { ExceptionItemCardHeader } from './header';
|
||||
import { ExceptionItemCardConditions } from './conditions';
|
||||
import { ExceptionItemCardMetaInfo } from './meta';
|
||||
import type { RuleReferenceSchema } from '../../../../../common/detection_engine/schemas/response';
|
||||
import type { ExceptionListRuleReferencesSchema } from '../../../../../common/detection_engine/schemas/response';
|
||||
import { ExceptionItemCardComments } from './comments';
|
||||
|
||||
export interface ExceptionItemProps {
|
||||
exceptionItem: ExceptionListItemSchema;
|
||||
listType: ExceptionListTypeEnum;
|
||||
disableActions: boolean;
|
||||
ruleReferences: RuleReferenceSchema[];
|
||||
ruleReferences: ExceptionListRuleReferencesSchema | null;
|
||||
onDeleteException: (arg: ExceptionListItemIdentifiers) => void;
|
||||
onEditException: (item: ExceptionListItemSchema) => void;
|
||||
dataTestSubj: string;
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock';
|
||||
|
||||
import { ExceptionItemCardMetaInfo } from './meta';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
|
@ -18,27 +20,30 @@ describe('ExceptionItemCardMetaInfo', () => {
|
|||
<TestProviders>
|
||||
<ExceptionItemCardMetaInfo
|
||||
item={getExceptionListItemSchemaMock()}
|
||||
references={[
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
},
|
||||
]}
|
||||
references={{
|
||||
...getExceptionListSchemaMock(),
|
||||
referenced_rules: [
|
||||
{
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
dataTestSubj="exceptionItemMeta"
|
||||
/>
|
||||
</TestProviders>
|
||||
|
@ -57,27 +62,30 @@ describe('ExceptionItemCardMetaInfo', () => {
|
|||
<TestProviders>
|
||||
<ExceptionItemCardMetaInfo
|
||||
item={getExceptionListItemSchemaMock()}
|
||||
references={[
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
},
|
||||
]}
|
||||
references={{
|
||||
...getExceptionListSchemaMock(),
|
||||
referenced_rules: [
|
||||
{
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
dataTestSubj="exceptionItemMeta"
|
||||
/>
|
||||
</TestProviders>
|
||||
|
@ -96,27 +104,30 @@ describe('ExceptionItemCardMetaInfo', () => {
|
|||
<TestProviders>
|
||||
<ExceptionItemCardMetaInfo
|
||||
item={getExceptionListItemSchemaMock()}
|
||||
references={[
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
},
|
||||
]}
|
||||
references={{
|
||||
...getExceptionListSchemaMock(),
|
||||
referenced_rules: [
|
||||
{
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
dataTestSubj="exceptionItemMeta"
|
||||
/>
|
||||
</TestProviders>
|
||||
|
@ -132,46 +143,49 @@ describe('ExceptionItemCardMetaInfo', () => {
|
|||
<TestProviders>
|
||||
<ExceptionItemCardMetaInfo
|
||||
item={getExceptionListItemSchemaMock()}
|
||||
references={[
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
},
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: 'aaa',
|
||||
name: 'Simple Rule Query 2',
|
||||
rule_id: 'rule-3',
|
||||
},
|
||||
]}
|
||||
references={{
|
||||
...getExceptionListSchemaMock(),
|
||||
referenced_rules: [
|
||||
{
|
||||
id: '1a2b3c',
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: 'aaa',
|
||||
name: 'Simple Rule Query 2',
|
||||
rule_id: 'rule-3',
|
||||
},
|
||||
],
|
||||
}}
|
||||
dataTestSubj="exceptionItemMeta"
|
||||
/>
|
||||
</TestProviders>
|
||||
|
|
|
@ -24,7 +24,7 @@ import styled from 'styled-components';
|
|||
import * as i18n from './translations';
|
||||
import { FormattedDate } from '../../../../common/components/formatted_date';
|
||||
import { SecurityPageName } from '../../../../../common/constants';
|
||||
import type { RuleReferenceSchema } from '../../../../../common/detection_engine/schemas/response';
|
||||
import type { ExceptionListRuleReferencesSchema } from '../../../../../common/detection_engine/schemas/response';
|
||||
import { SecuritySolutionLinkAnchor } from '../../../../common/components/links';
|
||||
import { RuleDetailTabs } from '../../../../detections/pages/detection_engine/rules/details';
|
||||
import { getRuleDetailsTabUrl } from '../../../../common/components/link_to/redirect_to_detection_engine';
|
||||
|
@ -36,7 +36,7 @@ const StyledFlexItem = styled(EuiFlexItem)`
|
|||
|
||||
export interface ExceptionItemCardMetaInfoProps {
|
||||
item: ExceptionListItemSchema;
|
||||
references: RuleReferenceSchema[];
|
||||
references: ExceptionListRuleReferencesSchema | null;
|
||||
dataTestSubj: string;
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@ export const ExceptionItemCardMetaInfo = memo<ExceptionItemCardMetaInfoProps>(
|
|||
if (references == null) {
|
||||
return [];
|
||||
}
|
||||
return references.map((reference) => (
|
||||
return references.referenced_rules.map((reference) => (
|
||||
<EuiContextMenuItem
|
||||
data-test-subj={`${dataTestSubj}-actionItem-${reference.id}`}
|
||||
key={reference.id}
|
||||
|
@ -94,25 +94,27 @@ export const ExceptionItemCardMetaInfo = memo<ExceptionItemCardMetaInfoProps>(
|
|||
dataTestSubj={`${dataTestSubj}-updatedBy`}
|
||||
/>
|
||||
</StyledFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButtonEmpty
|
||||
onClick={onAffectedRulesClick}
|
||||
iconType="list"
|
||||
data-test-subj={`${dataTestSubj}-affectedRulesButton`}
|
||||
>
|
||||
{i18n.AFFECTED_RULES(references?.length ?? 0)}
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
panelPaddingSize="none"
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={onClosePopover}
|
||||
data-test-subj={`${dataTestSubj}-items`}
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={itemActions} />
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
{references != null && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButtonEmpty
|
||||
onClick={onAffectedRulesClick}
|
||||
iconType="list"
|
||||
data-test-subj={`${dataTestSubj}-affectedRulesButton`}
|
||||
>
|
||||
{i18n.AFFECTED_RULES(references?.referenced_rules.length ?? 0)}
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
panelPaddingSize="none"
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={onClosePopover}
|
||||
data-test-subj={`${dataTestSubj}-items`}
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={itemActions} />
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { getRulesSchemaMock } from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
|
||||
import type { Rule } from '../../../../../detections/containers/detection_engine/rules/types';
|
||||
|
||||
import { ExceptionsAddToRulesOrLists } from '.';
|
||||
|
||||
describe('ExceptionsAddToRulesOrLists', () => {
|
||||
it('it passes empty array for shared lists attached if no rules passed in', () => {
|
||||
const wrapper = shallow(
|
||||
<ExceptionsAddToRulesOrLists
|
||||
rules={null}
|
||||
isBulkAction={false}
|
||||
selectedRadioOption="add_to_rule"
|
||||
onListSelectionChange={jest.fn()}
|
||||
onRuleSelectionChange={jest.fn()}
|
||||
onRadioChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('ExceptionsAddToListsOptions').prop('sharedLists')).toEqual([]);
|
||||
expect(wrapper.find('ExceptionsAddToListsOptions').prop('rulesCount')).toEqual(0);
|
||||
});
|
||||
|
||||
it('it passes all shared lists attached to a single rule', () => {
|
||||
const wrapper = shallow(
|
||||
<ExceptionsAddToRulesOrLists
|
||||
rules={[
|
||||
{
|
||||
...getRulesSchemaMock(),
|
||||
exceptions_list: [
|
||||
{ id: '123', list_id: 'my_list', namespace_type: 'single', type: 'detection' },
|
||||
],
|
||||
} as Rule,
|
||||
]}
|
||||
isBulkAction={false}
|
||||
selectedRadioOption="add_to_rule"
|
||||
onListSelectionChange={jest.fn()}
|
||||
onRuleSelectionChange={jest.fn()}
|
||||
onRadioChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('ExceptionsAddToListsOptions').prop('sharedLists')).toEqual([
|
||||
{ id: '123', list_id: 'my_list', namespace_type: 'single', type: 'detection' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('it passes shared lists that are in common if multiple rules exist', () => {
|
||||
const wrapper = shallow(
|
||||
<ExceptionsAddToRulesOrLists
|
||||
rules={[
|
||||
{
|
||||
...getRulesSchemaMock(),
|
||||
exceptions_list: [
|
||||
{ id: '123', list_id: 'my_list', namespace_type: 'single', type: 'detection' },
|
||||
],
|
||||
} as Rule,
|
||||
{
|
||||
...getRulesSchemaMock(),
|
||||
id: '2',
|
||||
rule_id: '2',
|
||||
exceptions_list: [
|
||||
{ id: '123', list_id: 'my_list', namespace_type: 'single', type: 'detection' },
|
||||
{ id: '456', list_id: 'my_list_2', namespace_type: 'single', type: 'detection' },
|
||||
{ id: '789', list_id: 'my_list_3', namespace_type: 'single', type: 'detection' },
|
||||
],
|
||||
} as Rule,
|
||||
{
|
||||
...getRulesSchemaMock(),
|
||||
id: '3',
|
||||
rule_id: '3',
|
||||
exceptions_list: [
|
||||
{ id: '123', list_id: 'my_list', namespace_type: 'single', type: 'detection' },
|
||||
{ id: '789', list_id: 'my_list_3', namespace_type: 'single', type: 'detection' },
|
||||
],
|
||||
} as Rule,
|
||||
]}
|
||||
isBulkAction={false}
|
||||
selectedRadioOption="add_to_rule"
|
||||
onListSelectionChange={jest.fn()}
|
||||
onRuleSelectionChange={jest.fn()}
|
||||
onRadioChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('ExceptionsAddToListsOptions').prop('sharedLists')).toEqual([
|
||||
{ id: '123', list_id: 'my_list', namespace_type: 'single', type: 'detection' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('it passes an empty array for shared lists if multiple rules exist and they have no shared lists in common', () => {
|
||||
const wrapper = shallow(
|
||||
<ExceptionsAddToRulesOrLists
|
||||
rules={[
|
||||
{
|
||||
...getRulesSchemaMock(),
|
||||
exceptions_list: [
|
||||
{ id: '123', list_id: 'my_list', namespace_type: 'single', type: 'detection' },
|
||||
],
|
||||
} as Rule,
|
||||
{
|
||||
...getRulesSchemaMock(),
|
||||
id: '2',
|
||||
rule_id: '2',
|
||||
exceptions_list: [
|
||||
{ id: '456', list_id: 'my_list_2', namespace_type: 'single', type: 'detection' },
|
||||
{ id: '789', list_id: 'my_list_3', namespace_type: 'single', type: 'detection' },
|
||||
],
|
||||
} as Rule,
|
||||
{
|
||||
...getRulesSchemaMock(),
|
||||
id: '3',
|
||||
rule_id: '3',
|
||||
exceptions_list: [
|
||||
{ id: '123', list_id: 'my_list', namespace_type: 'single', type: 'detection' },
|
||||
{ id: '789', list_id: 'my_list_3', namespace_type: 'single', type: 'detection' },
|
||||
],
|
||||
} as Rule,
|
||||
]}
|
||||
isBulkAction={false}
|
||||
selectedRadioOption="add_to_rule"
|
||||
onListSelectionChange={jest.fn()}
|
||||
onRuleSelectionChange={jest.fn()}
|
||||
onRadioChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('ExceptionsAddToListsOptions').prop('sharedLists')).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { EuiTitle, EuiSpacer, EuiPanel } from '@elastic/eui';
|
||||
import styled, { css } from 'styled-components';
|
||||
import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import type { Rule } from '../../../../../detections/containers/detection_engine/rules/types';
|
||||
import { ExceptionsAddToRulesOptions } from '../add_to_rules_options';
|
||||
import { ExceptionsAddToListsOptions } from '../add_to_lists_options';
|
||||
|
||||
interface ExceptionsAddToRulesOrListsComponentProps {
|
||||
/* Rules that exception item will be added to, or whose shared lists will be used to populate add to lists option. If none passed in, user is prompted to select what rules to add exception to. */
|
||||
rules: Rule[] | null;
|
||||
selectedRadioOption: string;
|
||||
/* Is user adding an exception item from the rules bulk actions */
|
||||
isBulkAction: boolean;
|
||||
onListSelectionChange: (lists: ExceptionListSchema[]) => void;
|
||||
onRuleSelectionChange: (rulesSelectedToAdd: Rule[]) => void;
|
||||
onRadioChange: (radioId: string) => void;
|
||||
}
|
||||
|
||||
const SectionHeader = styled(EuiTitle)`
|
||||
${() => css`
|
||||
font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
|
||||
`}
|
||||
`;
|
||||
|
||||
const ExceptionsAddToRulesOrListsComponent: React.FC<ExceptionsAddToRulesOrListsComponentProps> = ({
|
||||
rules,
|
||||
isBulkAction,
|
||||
selectedRadioOption,
|
||||
onListSelectionChange,
|
||||
onRuleSelectionChange,
|
||||
onRadioChange,
|
||||
}): JSX.Element => {
|
||||
const isSingleRule = useMemo(() => rules != null && rules.length === 1, [rules]);
|
||||
|
||||
/*
|
||||
* Determine what shared lists to display as selectable options for adding the exception item to:
|
||||
* - if dealing with a single rule - show any shared exception lists it has attached
|
||||
* - if dealing with multiple rules - show only shared exception lists that are common
|
||||
* across all of the rules
|
||||
*/
|
||||
const sharedLists = useMemo(() => {
|
||||
if (rules == null) return [];
|
||||
|
||||
if (rules.length === 1)
|
||||
return (
|
||||
rules[0].exceptions_list?.filter((list) => list.type === ExceptionListTypeEnum.DETECTION) ??
|
||||
[]
|
||||
);
|
||||
|
||||
const lists =
|
||||
rules?.map((rule) => (rule.exceptions_list != null ? rule.exceptions_list : [])) ?? [];
|
||||
|
||||
lists.sort((a, b) => {
|
||||
return a.length - b.length;
|
||||
});
|
||||
|
||||
const shortestArrOfLists = lists.shift();
|
||||
|
||||
if (shortestArrOfLists == null || !shortestArrOfLists.length) return [];
|
||||
|
||||
return shortestArrOfLists
|
||||
.filter((exceptionListInfo) =>
|
||||
lists.every((l) => l.some(({ id }) => exceptionListInfo.id === id))
|
||||
)
|
||||
.filter((list) => list.type === ExceptionListTypeEnum.DETECTION);
|
||||
}, [rules]);
|
||||
const rulesCount = useMemo(() => (rules != null ? rules.length : 0), [rules]);
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
paddingSize="none"
|
||||
hasShadow={false}
|
||||
data-test-subj="exceptionItemAddToRuleOrListSection"
|
||||
>
|
||||
<SectionHeader size="xs">
|
||||
<h3>{i18n.ADD_TO_LISTS_SECTION_TITLE}</h3>
|
||||
</SectionHeader>
|
||||
<EuiSpacer size="s" />
|
||||
<ExceptionsAddToRulesOptions
|
||||
possibleRules={rules}
|
||||
isSingleRule={isSingleRule}
|
||||
isBulkAction={isBulkAction}
|
||||
selectedRadioOption={selectedRadioOption}
|
||||
onRuleSelectionChange={onRuleSelectionChange}
|
||||
onRadioChange={onRadioChange}
|
||||
/>
|
||||
<ExceptionsAddToListsOptions
|
||||
rulesCount={rulesCount}
|
||||
selectedRadioOption={selectedRadioOption}
|
||||
sharedLists={sharedLists}
|
||||
onListsSelectionChange={onListSelectionChange}
|
||||
onRadioChange={onRadioChange}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExceptionsAddToRulesOrLists = React.memo(ExceptionsAddToRulesOrListsComponent);
|
||||
|
||||
ExceptionsAddToRulesOrLists.displayName = 'ExceptionsAddToRulesOrLists';
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 ADD_TO_LISTS_SECTION_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.addExceptionToRuleOrList.addToListsLabel',
|
||||
{
|
||||
defaultMessage: 'Add to rule or lists',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { ExceptionsAddToListsOptions } from '.';
|
||||
|
||||
jest.mock('../../../../../detections/pages/detection_engine/rules/all/rules_table/use_find_rules');
|
||||
|
||||
describe('ExceptionsAddToListsOptions', () => {
|
||||
it('it displays radio option as disabled if there are no "sharedLists"', () => {
|
||||
const wrapper = shallow(
|
||||
<ExceptionsAddToListsOptions
|
||||
rulesCount={1}
|
||||
selectedRadioOption="add_to_rule"
|
||||
sharedLists={[]}
|
||||
onListsSelectionChange={jest.fn()}
|
||||
onRadioChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionsAddToListTable"]').exists()).toBeFalsy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="addToListsRadioOption"]').at(0).props().disabled
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it displays lists table if radio is selected', () => {
|
||||
const wrapper = shallow(
|
||||
<ExceptionsAddToListsOptions
|
||||
rulesCount={1}
|
||||
selectedRadioOption="add_to_lists"
|
||||
sharedLists={[
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'my_list_id',
|
||||
namespace_type: 'single',
|
||||
type: ExceptionListTypeEnum.DETECTION,
|
||||
},
|
||||
]}
|
||||
onListsSelectionChange={jest.fn()}
|
||||
onRadioChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="addToListsRadioOption"]').at(0).props().disabled
|
||||
).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="exceptionsAddToListTable"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it does not display lists table if radio is not selected', () => {
|
||||
const wrapper = shallow(
|
||||
<ExceptionsAddToListsOptions
|
||||
rulesCount={1}
|
||||
selectedRadioOption="add_to_rule"
|
||||
sharedLists={[
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'my_list_id',
|
||||
namespace_type: 'single',
|
||||
type: ExceptionListTypeEnum.DETECTION,
|
||||
},
|
||||
]}
|
||||
onListsSelectionChange={jest.fn()}
|
||||
onRadioChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionsAddToListTable"]').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { EuiText, EuiRadio, EuiFlexItem, EuiFlexGroup, EuiIconTip } from '@elastic/eui';
|
||||
|
||||
import type { ExceptionListSchema, ListArray } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import * as i18n from './translations';
|
||||
import { ExceptionsAddToListsTable } from '../add_to_lists_table';
|
||||
|
||||
interface ExceptionsAddToListsOptionsComponentProps {
|
||||
rulesCount: number;
|
||||
selectedRadioOption: string;
|
||||
sharedLists: ListArray;
|
||||
onListsSelectionChange: (listsSelectedToAdd: ExceptionListSchema[]) => void;
|
||||
onRadioChange: (option: string) => void;
|
||||
}
|
||||
|
||||
const ExceptionsAddToListsOptionsComponent: React.FC<ExceptionsAddToListsOptionsComponentProps> = ({
|
||||
rulesCount,
|
||||
selectedRadioOption,
|
||||
sharedLists,
|
||||
onListsSelectionChange,
|
||||
onRadioChange,
|
||||
}): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<EuiRadio
|
||||
id="add_to_lists"
|
||||
label={
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
ata-test-subj="addToListsRadioOptionLabel"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>{i18n.ADD_TO_LISTS_OPTION}</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false} data-test-subj="addToListsOption">
|
||||
<EuiIconTip
|
||||
content={
|
||||
sharedLists.length === 0
|
||||
? i18n.ADD_TO_LISTS_OPTION_DISABLED_TOOLTIP(rulesCount)
|
||||
: i18n.ADD_TO_LISTS_OPTION_TOOLTIP
|
||||
}
|
||||
title={i18n.ADD_TO_LISTS_OPTION}
|
||||
position="top"
|
||||
type="iInCircle"
|
||||
data-test-subj="addToListsOptionTooltip"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
checked={selectedRadioOption === 'add_to_lists'}
|
||||
disabled={sharedLists.length === 0 && rulesCount > 0}
|
||||
onChange={() => onRadioChange('add_to_lists')}
|
||||
data-test-subj="addToListsRadioOption"
|
||||
/>
|
||||
|
||||
{selectedRadioOption === 'add_to_lists' && (sharedLists.length > 0 || rulesCount === 0) && (
|
||||
<ExceptionsAddToListsTable
|
||||
showAllSharedLists={rulesCount === 0}
|
||||
sharedExceptionLists={sharedLists}
|
||||
onListSelectionChange={onListsSelectionChange}
|
||||
data-test-subj="exceptionsAddToListTable"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExceptionsAddToListsOptions = React.memo(ExceptionsAddToListsOptionsComponent);
|
||||
|
||||
ExceptionsAddToListsOptions.displayName = 'ExceptionsAddToListsOptions';
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 ADD_TO_LISTS_OPTION = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.addToListsOptions.addToListsOptionLabel',
|
||||
{
|
||||
defaultMessage: 'Add to shared exception lists',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_LISTS_OPTION_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.addToListsOptions.addToListsTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'Shared exception list is a group of exceptions. Select this option if you’d like to add this exception to shared exception lists.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_TO_LISTS_OPTION_DISABLED_TOOLTIP = (rulesCount: number) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.addToListsOptions.addToListsTooltipTitle',
|
||||
{
|
||||
values: { rulesCount },
|
||||
defaultMessage:
|
||||
'Shared exception list is a group of exceptions. {rulesCount, plural, =1 {This rule currently has no shared} other {These rules currently have no commonly shared}} exception lists attached. To create one, visit the Exception lists management page.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { ExceptionsAddToListsTable } from '.';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { useFindExceptionListReferences } from '../../../logic/use_find_references';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { mount } from 'enzyme';
|
||||
import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock';
|
||||
|
||||
jest.mock('../../../logic/use_find_references');
|
||||
|
||||
describe('ExceptionsAddToListsTable', () => {
|
||||
const mockFn = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
(useFindExceptionListReferences as jest.Mock).mockReturnValue([
|
||||
false,
|
||||
false,
|
||||
{
|
||||
my_list_id: {
|
||||
...getExceptionListSchemaMock(),
|
||||
id: '123',
|
||||
list_id: 'my_list_id',
|
||||
namespace_type: 'single',
|
||||
type: ExceptionListTypeEnum.DETECTION,
|
||||
name: 'My exception list',
|
||||
referenced_rules: [
|
||||
{
|
||||
id: '345',
|
||||
name: 'My rule',
|
||||
rule_id: 'my_rule_id',
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'my_list_id',
|
||||
namespace_type: 'single',
|
||||
type: ExceptionListTypeEnum.DETECTION,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
mockFn,
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('it displays loading state while fetching data', () => {
|
||||
(useFindExceptionListReferences as jest.Mock).mockReturnValue([true, false, null, mockFn]);
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ExceptionsAddToListsTable
|
||||
showAllSharedLists={false}
|
||||
sharedExceptionLists={[
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'my_list_id',
|
||||
namespace_type: 'single',
|
||||
type: ExceptionListTypeEnum.DETECTION,
|
||||
},
|
||||
]}
|
||||
onListSelectionChange={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(mockFn).toHaveBeenCalledWith([
|
||||
{
|
||||
id: '123',
|
||||
listId: 'my_list_id',
|
||||
namespaceType: 'single',
|
||||
},
|
||||
]);
|
||||
expect(wrapper.find('[data-test-subj="exceptionItemListsTableLoading"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it displays error state if fetching list and references data fails', () => {
|
||||
(useFindExceptionListReferences as jest.Mock).mockReturnValue([false, true, null, jest.fn()]);
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ExceptionsAddToListsTable
|
||||
showAllSharedLists={false}
|
||||
sharedExceptionLists={[
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'my_list_id',
|
||||
namespace_type: 'single',
|
||||
type: ExceptionListTypeEnum.DETECTION,
|
||||
},
|
||||
]}
|
||||
onListSelectionChange={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('EuiInMemoryTable').prop('error')).toEqual(
|
||||
'Unable to load shared exception lists'
|
||||
);
|
||||
});
|
||||
|
||||
it('it invokes "useFindExceptionListReferences" with array of namespace types to fetch all lists if "showAllSharedLists" is "true"', () => {
|
||||
mount(
|
||||
<TestProviders>
|
||||
<ExceptionsAddToListsTable
|
||||
showAllSharedLists
|
||||
sharedExceptionLists={[
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'my_list_id',
|
||||
namespace_type: 'single',
|
||||
type: ExceptionListTypeEnum.DETECTION,
|
||||
},
|
||||
]}
|
||||
onListSelectionChange={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(mockFn).toHaveBeenCalledWith([
|
||||
{ namespaceType: 'single' },
|
||||
{ namespaceType: 'agnostic' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('it displays lists with rule references', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ExceptionsAddToListsTable
|
||||
showAllSharedLists={false}
|
||||
sharedExceptionLists={[
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'my_list_id',
|
||||
namespace_type: 'single',
|
||||
type: ExceptionListTypeEnum.DETECTION,
|
||||
},
|
||||
]}
|
||||
onListSelectionChange={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="ruleReferencesDisplayPopoverButton"]').at(1).text()
|
||||
).toEqual('1');
|
||||
// Formatting is off since doesn't take css into account
|
||||
expect(wrapper.find('[data-test-subj="exceptionListNameCell"]').at(1).text()).toEqual(
|
||||
'NameMy exception list'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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, { useEffect, useState, useMemo } from 'react';
|
||||
import type { CriteriaWithPagination } from '@elastic/eui';
|
||||
import { EuiText, EuiSpacer, EuiInMemoryTable, EuiPanel, EuiLoadingContent } from '@elastic/eui';
|
||||
import type { ExceptionListSchema, ListArray } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import type { FindRulesReferencedByExceptionsListProp } from '../../../../../detections/containers/detection_engine/rules';
|
||||
import * as i18n from './translations';
|
||||
import { getSharedListsTableColumns } from '../utils';
|
||||
import { useFindExceptionListReferences } from '../../../logic/use_find_references';
|
||||
import type { ExceptionListRuleReferencesSchema } from '../../../../../../common/detection_engine/schemas/response';
|
||||
|
||||
interface ExceptionsAddToListsComponentProps {
|
||||
/**
|
||||
* Normally if there's no sharedExceptionLists, this opition is disabled, however,
|
||||
* when adding an exception item from the exception lists management page, there is no
|
||||
* list or rule to go off of, so user can select to add the exception to any rule or to any
|
||||
* shared list.
|
||||
*/
|
||||
showAllSharedLists: boolean;
|
||||
/* Shared exception lists to display as options to add item to */
|
||||
sharedExceptionLists: ListArray;
|
||||
onListSelectionChange?: (listsSelectedToAdd: ExceptionListSchema[]) => void;
|
||||
}
|
||||
|
||||
const ExceptionsAddToListsComponent: React.FC<ExceptionsAddToListsComponentProps> = ({
|
||||
showAllSharedLists,
|
||||
sharedExceptionLists,
|
||||
onListSelectionChange,
|
||||
}): JSX.Element => {
|
||||
const listsToFetch = useMemo(() => {
|
||||
return showAllSharedLists ? [] : sharedExceptionLists;
|
||||
}, [showAllSharedLists, sharedExceptionLists]);
|
||||
const [listsToDisplay, setListsToDisplay] = useState<ExceptionListRuleReferencesSchema[]>([]);
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0 });
|
||||
const [message, setMessage] = useState<JSX.Element | string | undefined>(
|
||||
<EuiLoadingContent lines={4} data-test-subj="exceptionItemListsTableLoading" />
|
||||
);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const [isLoadingReferences, referenceFetchError, ruleReferences, fetchReferences] =
|
||||
useFindExceptionListReferences();
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchReferences != null) {
|
||||
const listsToQuery: FindRulesReferencedByExceptionsListProp[] = !listsToFetch.length
|
||||
? [{ namespaceType: 'single' }, { namespaceType: 'agnostic' }]
|
||||
: listsToFetch.map(({ id, list_id: listId, namespace_type: namespaceType }) => ({
|
||||
id,
|
||||
listId,
|
||||
namespaceType,
|
||||
}));
|
||||
fetchReferences(listsToQuery);
|
||||
}
|
||||
}, [listsToFetch, fetchReferences]);
|
||||
|
||||
useEffect(() => {
|
||||
if (referenceFetchError) return setError(i18n.REFERENCES_FETCH_ERROR);
|
||||
if (isLoadingReferences) {
|
||||
return setMessage(
|
||||
<EuiLoadingContent lines={4} data-test-subj="exceptionItemListsTableLoading" />
|
||||
);
|
||||
}
|
||||
if (!ruleReferences) return;
|
||||
const lists: ExceptionListRuleReferencesSchema[] = [];
|
||||
for (const [_, value] of Object.entries(ruleReferences))
|
||||
if (value.type === ExceptionListTypeEnum.DETECTION) lists.push(value);
|
||||
|
||||
setMessage(undefined);
|
||||
setListsToDisplay(lists);
|
||||
}, [isLoadingReferences, referenceFetchError, ruleReferences, showAllSharedLists]);
|
||||
|
||||
const selectionValue = {
|
||||
onSelectionChange: (selection: ExceptionListRuleReferencesSchema[]) => {
|
||||
if (onListSelectionChange != null) {
|
||||
onListSelectionChange(
|
||||
selection.map(
|
||||
({
|
||||
referenced_rules: _,
|
||||
namespace_type: namespaceType,
|
||||
os_types: osTypes,
|
||||
tags,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
namespace_type: namespaceType ?? 'single',
|
||||
os_types: osTypes ?? [],
|
||||
tags: tags ?? [],
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
initialSelected: [],
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPanel color="subdued" borderRadius="none" hasShadow={false}>
|
||||
<>
|
||||
<EuiText size="s">{i18n.ADD_TO_LISTS_DESCRIPTION}</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSpacer size="s" />
|
||||
<EuiInMemoryTable<ExceptionListRuleReferencesSchema>
|
||||
tableCaption="Table of exception lists"
|
||||
itemId="id"
|
||||
items={listsToDisplay}
|
||||
loading={message != null}
|
||||
message={message}
|
||||
columns={getSharedListsTableColumns()}
|
||||
error={error}
|
||||
pagination={{
|
||||
...pagination,
|
||||
pageSizeOptions: [5],
|
||||
showPerPageOptions: false,
|
||||
}}
|
||||
onTableChange={({ page: { index } }: CriteriaWithPagination<never>) =>
|
||||
setPagination({ pageIndex: index })
|
||||
}
|
||||
selection={selectionValue}
|
||||
isSelectable
|
||||
sorting
|
||||
data-test-subj="addExceptionToSharedListsTable"
|
||||
/>
|
||||
</>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExceptionsAddToListsTable = React.memo(ExceptionsAddToListsComponent);
|
||||
|
||||
ExceptionsAddToListsTable.displayName = 'ExceptionsAddToListsTable';
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 ADD_TO_LISTS_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.addToListsTableSelection.addToListsDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Select shared exception list to add to. We will make a copy of this exception if multiple lists are selected.',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_LIST_DETAIL_ACTION = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.addToListsTableSelection.viewListDetailActionLabel',
|
||||
{
|
||||
defaultMessage: 'View list detail',
|
||||
}
|
||||
);
|
||||
|
||||
export const REFERENCES_FETCH_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.addToListsTableSelection.referencesFetchError',
|
||||
{
|
||||
defaultMessage: 'Unable to load shared exception lists',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { ExceptionsAddToRulesOptions } from '.';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { useFindRules } from '../../../../../detections/pages/detection_engine/rules/all/rules_table/use_find_rules';
|
||||
import { getRulesSchemaMock } from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
|
||||
import type { Rule } from '../../../../../detections/containers/detection_engine/rules/types';
|
||||
|
||||
jest.mock('../../../../../detections/pages/detection_engine/rules/all/rules_table/use_find_rules');
|
||||
|
||||
describe('ExceptionsAddToRulesOptions', () => {
|
||||
beforeEach(() => {
|
||||
(useFindRules as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
rules: [getRulesSchemaMock(), { ...getRulesSchemaMock(), id: '345', name: 'My rule' }],
|
||||
total: 0,
|
||||
},
|
||||
isFetched: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('it displays option to add exception to single rule if a single rule is passed in', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionsAddToRulesOptions
|
||||
possibleRules={[getRulesSchemaMock() as Rule]}
|
||||
isSingleRule
|
||||
isBulkAction={false}
|
||||
selectedRadioOption="add_to_rule"
|
||||
onRuleSelectionChange={jest.fn()}
|
||||
onRadioChange={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="addToRuleRadioOption"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it displays option to add exception to multiple rules if "isBulkAction" is "true" and rules are passed in', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionsAddToRulesOptions
|
||||
possibleRules={[getRulesSchemaMock() as Rule]}
|
||||
isSingleRule={false}
|
||||
isBulkAction
|
||||
selectedRadioOption="add_to_rules"
|
||||
onRuleSelectionChange={jest.fn()}
|
||||
onRadioChange={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="addToRulesRadioOption"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it displays rules selection table if "isBulkAction" is "true" and rules are passed in', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionsAddToRulesOptions
|
||||
possibleRules={null}
|
||||
isSingleRule={false}
|
||||
isBulkAction={false}
|
||||
selectedRadioOption="select_rules_to_add_to"
|
||||
onRuleSelectionChange={jest.fn()}
|
||||
onRadioChange={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="selectRulesToAddToRadioOption"]').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { EuiRadio, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { Rule } from '../../../../../detections/containers/detection_engine/rules/types';
|
||||
import { ExceptionsAddToRulesTable } from '../add_to_rules_table';
|
||||
|
||||
export type AddToRuleListsRadioOptions = 'select_rules_to_add_to' | 'add_to_rules' | 'add_to_rule';
|
||||
|
||||
interface ExceptionsAddToRulesOptionsComponentProps {
|
||||
possibleRules: Rule[] | null;
|
||||
selectedRadioOption: string;
|
||||
isSingleRule: boolean;
|
||||
isBulkAction: boolean;
|
||||
onRuleSelectionChange: (rulesSelectedToAdd: Rule[]) => void;
|
||||
onRadioChange: (option: AddToRuleListsRadioOptions) => void;
|
||||
}
|
||||
|
||||
const ExceptionsAddToRulesOptionsComponent: React.FC<ExceptionsAddToRulesOptionsComponentProps> = ({
|
||||
possibleRules,
|
||||
isSingleRule,
|
||||
isBulkAction,
|
||||
selectedRadioOption,
|
||||
onRuleSelectionChange,
|
||||
onRadioChange,
|
||||
}): JSX.Element => {
|
||||
const ruleRadioOptionProps = useMemo(() => {
|
||||
if (isBulkAction && possibleRules != null) {
|
||||
return {
|
||||
id: 'add_to_rules',
|
||||
label: (
|
||||
<EuiText data-test-subj="addToRulesRadioOption">
|
||||
<FormattedMessage
|
||||
defaultMessage="Add to [{numRules}] selected rules: {ruleNames}"
|
||||
id="xpack.securitySolution.exceptions.common.addToRulesOptionLabel"
|
||||
values={{
|
||||
numRules: possibleRules.length,
|
||||
ruleNames: (
|
||||
<span style={{ fontWeight: 'bold' }}>
|
||||
{possibleRules.map(({ name }) => name).join(',')}
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
checked: selectedRadioOption === 'add_to_rules',
|
||||
'data-test-subj': 'addToRulesOptionsRadio',
|
||||
onChange: () => {
|
||||
onRadioChange('add_to_rules');
|
||||
onRuleSelectionChange(possibleRules);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (isSingleRule && possibleRules != null) {
|
||||
return {
|
||||
id: 'add_to_rule',
|
||||
label: (
|
||||
<EuiText data-test-subj="addToRuleRadioOption">
|
||||
<FormattedMessage
|
||||
defaultMessage="Add to this rule: {ruleName}"
|
||||
id="xpack.securitySolution.exceptions.common.addToRuleOptionLabel"
|
||||
values={{
|
||||
ruleName: <span style={{ fontWeight: 'bold' }}>{possibleRules[0].name}</span>,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
checked: selectedRadioOption === 'add_to_rule',
|
||||
'data-test-subj': 'addToRuleOptionsRadio',
|
||||
onChange: () => {
|
||||
onRadioChange('add_to_rule');
|
||||
onRuleSelectionChange(possibleRules);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'select_rules_to_add_to',
|
||||
label: (
|
||||
<EuiText data-test-subj="selectRulesToAddToRadioOption">
|
||||
<FormattedMessage
|
||||
defaultMessage="Add to rules"
|
||||
id="xpack.securitySolution.exceptions.common.selectRulesOptionLabel"
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
checked: selectedRadioOption === 'select_rules_to_add_to',
|
||||
'data-test-subj': 'selectRulesToAddToOptionRadio',
|
||||
onChange: () => onRadioChange('select_rules_to_add_to'),
|
||||
};
|
||||
}, [
|
||||
isBulkAction,
|
||||
possibleRules,
|
||||
isSingleRule,
|
||||
selectedRadioOption,
|
||||
onRadioChange,
|
||||
onRuleSelectionChange,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiRadio {...ruleRadioOptionProps} />
|
||||
{selectedRadioOption === 'select_rules_to_add_to' && (
|
||||
<ExceptionsAddToRulesTable
|
||||
onRuleSelectionChange={onRuleSelectionChange}
|
||||
initiallySelectedRules={[]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExceptionsAddToRulesOptions = React.memo(ExceptionsAddToRulesOptionsComponent);
|
||||
|
||||
ExceptionsAddToRulesOptions.displayName = 'ExceptionsAddToRulesOptions';
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { ExceptionsAddToRulesTable } from '.';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { useFindRules } from '../../../../../detections/pages/detection_engine/rules/all/rules_table/use_find_rules';
|
||||
import { getRulesSchemaMock } from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
|
||||
import type { Rule } from '../../../../../detections/containers/detection_engine/rules/types';
|
||||
|
||||
jest.mock('../../../../../detections/pages/detection_engine/rules/all/rules_table/use_find_rules');
|
||||
|
||||
describe('ExceptionsAddToRulesTable', () => {
|
||||
it('it displays loading state while fetching rules', () => {
|
||||
(useFindRules as jest.Mock).mockReturnValue({
|
||||
data: { rules: [], total: 0 },
|
||||
isFetched: false,
|
||||
});
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionsAddToRulesTable initiallySelectedRules={[]} onRuleSelectionChange={jest.fn()} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-loading"]').exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it displays fetched rules', () => {
|
||||
(useFindRules as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
rules: [getRulesSchemaMock(), { ...getRulesSchemaMock(), id: '345', name: 'My rule' }],
|
||||
total: 0,
|
||||
},
|
||||
isFetched: true,
|
||||
});
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionsAddToRulesTable
|
||||
initiallySelectedRules={[{ ...getRulesSchemaMock(), id: '345', name: 'My rule' } as Rule]}
|
||||
onRuleSelectionChange={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-loading"]').exists()
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
wrapper.find('.euiTableRow-isSelected td[data-test-subj="ruleNameCell"]').text()
|
||||
).toEqual('NameMy rule');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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, { useEffect, useMemo, useState } from 'react';
|
||||
import type { CriteriaWithPagination } from '@elastic/eui';
|
||||
import { EuiSpacer, EuiPanel, EuiText, EuiInMemoryTable, EuiLoadingContent } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import * as myI18n from './translations';
|
||||
import type { Rule } from '../../../../../detections/containers/detection_engine/rules/types';
|
||||
import { useFindRules } from '../../../../../detections/pages/detection_engine/rules/all/rules_table/use_find_rules';
|
||||
import { getRulesTableColumn } from '../utils';
|
||||
|
||||
interface ExceptionsAddToRulesComponentProps {
|
||||
initiallySelectedRules?: Rule[];
|
||||
onRuleSelectionChange?: (rulesSelectedToAdd: Rule[]) => void;
|
||||
}
|
||||
|
||||
const ExceptionsAddToRulesTableComponent: React.FC<ExceptionsAddToRulesComponentProps> = ({
|
||||
initiallySelectedRules,
|
||||
onRuleSelectionChange,
|
||||
}) => {
|
||||
const { data: { rules } = { rules: [], total: 0 }, isFetched } = useFindRules({
|
||||
isInMemorySorting: true,
|
||||
filterOptions: {
|
||||
filter: '',
|
||||
showCustomRules: false,
|
||||
showElasticRules: false,
|
||||
tags: [],
|
||||
},
|
||||
sortingOptions: undefined,
|
||||
pagination: undefined,
|
||||
refetchInterval: false,
|
||||
});
|
||||
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0 });
|
||||
const [message, setMessage] = useState<JSX.Element | string | undefined>(
|
||||
<EuiLoadingContent lines={4} data-test-subj="exceptionItemViewerEmptyPrompts-loading" />
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetched) {
|
||||
setMessage(
|
||||
<EuiLoadingContent lines={4} data-test-subj="exceptionItemViewerEmptyPrompts-loading" />
|
||||
);
|
||||
}
|
||||
|
||||
if (isFetched) {
|
||||
setMessage(undefined);
|
||||
}
|
||||
}, [setMessage, isFetched]);
|
||||
|
||||
const ruleSelectionValue = {
|
||||
onSelectionChange: (selection: Rule[]) => {
|
||||
if (onRuleSelectionChange != null) {
|
||||
onRuleSelectionChange(selection);
|
||||
}
|
||||
},
|
||||
initialSelected: initiallySelectedRules ?? [],
|
||||
};
|
||||
|
||||
const searchOptions = useMemo(
|
||||
() => ({
|
||||
box: {
|
||||
incremental: true,
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
type: 'field_value_selection' as const,
|
||||
field: 'tags',
|
||||
name: i18n.translate(
|
||||
'xpack.securitySolution.exceptions.addToRulesTable.tagsFilterLabel',
|
||||
{
|
||||
defaultMessage: 'Tags',
|
||||
}
|
||||
),
|
||||
multiSelect: 'or' as const,
|
||||
options: rules.flatMap(({ tags }) => {
|
||||
return tags.map((tag) => ({
|
||||
value: tag,
|
||||
name: tag,
|
||||
}));
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
[rules]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel color="subdued" borderRadius="none" hasShadow={false}>
|
||||
<>
|
||||
<EuiText size="s">{myI18n.ADD_TO_SELECTED_RULES_DESCRIPTION}</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiInMemoryTable<Rule>
|
||||
tableCaption="Rules table"
|
||||
itemId="id"
|
||||
items={rules}
|
||||
loading={!isFetched}
|
||||
columns={getRulesTableColumn()}
|
||||
pagination={{
|
||||
...pagination,
|
||||
itemsPerPage: 5,
|
||||
showPerPageOptions: false,
|
||||
}}
|
||||
message={message}
|
||||
onTableChange={({ page: { index } }: CriteriaWithPagination<never>) =>
|
||||
setPagination({ pageIndex: index })
|
||||
}
|
||||
selection={ruleSelectionValue}
|
||||
search={searchOptions}
|
||||
sorting
|
||||
isSelectable
|
||||
data-test-subj="addExceptionToRulesTable"
|
||||
/>
|
||||
</>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExceptionsAddToRulesTable = React.memo(ExceptionsAddToRulesTableComponent);
|
||||
|
||||
ExceptionsAddToRulesTable.displayName = 'ExceptionsAddToRulesTable';
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 ADD_TO_SELECTED_RULES_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.addToRulesTableSelection.addToSelectedRulesDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Select rules add to. We will make a copy of this exception if it links to multiple rules. ',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { ExceptionItemsFlyoutAlertsActions } from '.';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
import type { AlertData } from '../../../utils/types';
|
||||
|
||||
jest.mock('../../../../../common/lib/kibana');
|
||||
|
||||
const alertDataMock: AlertData = {
|
||||
'@timestamp': '1234567890',
|
||||
_id: 'test-id',
|
||||
file: { path: 'test/path' },
|
||||
};
|
||||
|
||||
describe('ExceptionItemsFlyoutAlertsActions', () => {
|
||||
it('it displays single alert close checkbox if alert status is not "closed" and "alertData" exists', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionItemsFlyoutAlertsActions
|
||||
exceptionListItems={[getExceptionListItemSchemaMock()]}
|
||||
exceptionListType={ExceptionListTypeEnum.DETECTION}
|
||||
shouldCloseSingleAlert={false}
|
||||
shouldBulkCloseAlert={false}
|
||||
disableBulkClose={false}
|
||||
alertData={alertDataMock}
|
||||
alertStatus="open"
|
||||
onDisableBulkClose={jest.fn()}
|
||||
onUpdateBulkCloseIndex={jest.fn()}
|
||||
onBulkCloseCheckboxChange={jest.fn()}
|
||||
onSingleAlertCloseCheckboxChange={jest.fn()}
|
||||
isAlertDataLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="closeAlertOnAddExceptionCheckbox"]').exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it does not display single alert close checkbox if alert status is "closed"', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionItemsFlyoutAlertsActions
|
||||
exceptionListItems={[getExceptionListItemSchemaMock()]}
|
||||
exceptionListType={ExceptionListTypeEnum.DETECTION}
|
||||
shouldCloseSingleAlert={false}
|
||||
shouldBulkCloseAlert={false}
|
||||
disableBulkClose={false}
|
||||
alertData={alertDataMock}
|
||||
alertStatus="closed"
|
||||
onDisableBulkClose={jest.fn()}
|
||||
onUpdateBulkCloseIndex={jest.fn()}
|
||||
onBulkCloseCheckboxChange={jest.fn()}
|
||||
onSingleAlertCloseCheckboxChange={jest.fn()}
|
||||
isAlertDataLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="closeAlertOnAddExceptionCheckbox"]').exists()
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('it does not display single alert close checkbox if "alertData" does not exist', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionItemsFlyoutAlertsActions
|
||||
exceptionListItems={[getExceptionListItemSchemaMock()]}
|
||||
exceptionListType={ExceptionListTypeEnum.DETECTION}
|
||||
shouldCloseSingleAlert={false}
|
||||
shouldBulkCloseAlert={false}
|
||||
disableBulkClose={false}
|
||||
alertData={undefined}
|
||||
alertStatus="open"
|
||||
onDisableBulkClose={jest.fn()}
|
||||
onUpdateBulkCloseIndex={jest.fn()}
|
||||
onBulkCloseCheckboxChange={jest.fn()}
|
||||
onSingleAlertCloseCheckboxChange={jest.fn()}
|
||||
isAlertDataLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="closeAlertOnAddExceptionCheckbox"]').exists()
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('it displays bulk close checkbox', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionItemsFlyoutAlertsActions
|
||||
exceptionListItems={[getExceptionListItemSchemaMock()]}
|
||||
exceptionListType={ExceptionListTypeEnum.DETECTION}
|
||||
shouldCloseSingleAlert={false}
|
||||
shouldBulkCloseAlert={false}
|
||||
disableBulkClose={false}
|
||||
alertData={alertDataMock}
|
||||
alertStatus="open"
|
||||
onDisableBulkClose={jest.fn()}
|
||||
onUpdateBulkCloseIndex={jest.fn()}
|
||||
onBulkCloseCheckboxChange={jest.fn()}
|
||||
onSingleAlertCloseCheckboxChange={jest.fn()}
|
||||
isAlertDataLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="bulkCloseAlertOnAddExceptionCheckbox"]').exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it displays checkboxes disabled if "isAlertDataLoading" is "true"', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionItemsFlyoutAlertsActions
|
||||
exceptionListItems={[getExceptionListItemSchemaMock()]}
|
||||
exceptionListType={ExceptionListTypeEnum.DETECTION}
|
||||
shouldCloseSingleAlert={false}
|
||||
shouldBulkCloseAlert={false}
|
||||
disableBulkClose={false}
|
||||
alertData={alertDataMock}
|
||||
alertStatus="open"
|
||||
onDisableBulkClose={jest.fn()}
|
||||
onUpdateBulkCloseIndex={jest.fn()}
|
||||
onBulkCloseCheckboxChange={jest.fn()}
|
||||
onSingleAlertCloseCheckboxChange={jest.fn()}
|
||||
isAlertDataLoading
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="bulkCloseAlertOnAddExceptionCheckbox"]').at(0).props().disabled
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="closeAlertOnAddExceptionCheckbox"]').at(0).props().disabled
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it displays bulk close checkbox disabled if "disableBulkCloseAlert" is "true"', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionItemsFlyoutAlertsActions
|
||||
exceptionListItems={[getExceptionListItemSchemaMock()]}
|
||||
exceptionListType={ExceptionListTypeEnum.DETECTION}
|
||||
shouldCloseSingleAlert={false}
|
||||
shouldBulkCloseAlert={false}
|
||||
disableBulkClose={true}
|
||||
alertData={alertDataMock}
|
||||
alertStatus="open"
|
||||
onDisableBulkClose={jest.fn()}
|
||||
onUpdateBulkCloseIndex={jest.fn()}
|
||||
onBulkCloseCheckboxChange={jest.fn()}
|
||||
onSingleAlertCloseCheckboxChange={jest.fn()}
|
||||
isAlertDataLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="bulkCloseAlertOnAddExceptionCheckbox"]').at(0).props().disabled
|
||||
).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="addExceptionEndpointText"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('it displays endpoint quarantine text if exception list type is "endpoint"', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionItemsFlyoutAlertsActions
|
||||
exceptionListItems={[getExceptionListItemSchemaMock()]}
|
||||
exceptionListType={ExceptionListTypeEnum.ENDPOINT}
|
||||
shouldCloseSingleAlert={false}
|
||||
shouldBulkCloseAlert={false}
|
||||
disableBulkClose={false}
|
||||
alertData={alertDataMock}
|
||||
alertStatus="open"
|
||||
onDisableBulkClose={jest.fn()}
|
||||
onUpdateBulkCloseIndex={jest.fn()}
|
||||
onBulkCloseCheckboxChange={jest.fn()}
|
||||
onSingleAlertCloseCheckboxChange={jest.fn()}
|
||||
isAlertDataLoading={false}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="addExceptionEndpointText"]').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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, { useCallback, useEffect, useMemo } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { EuiTitle, EuiFormRow, EuiCheckbox, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type { ExceptionsBuilderReturnExceptionItem } from '@kbn/securitysolution-list-utils';
|
||||
|
||||
import { useSignalIndex } from '../../../../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
import type { Status } from '../../../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { useFetchIndex } from '../../../../../common/containers/source';
|
||||
import { entryHasListType, entryHasNonEcsType } from './utils';
|
||||
import * as i18n from './translations';
|
||||
import type { AlertData } from '../../../utils/types';
|
||||
|
||||
const FlyoutCheckboxesSection = styled.section`
|
||||
overflow-y: inherit;
|
||||
height: auto;
|
||||
.euiFlyoutBody__overflowContent {
|
||||
padding-top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const SectionHeader = styled(EuiTitle)`
|
||||
${() => css`
|
||||
font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
|
||||
`}
|
||||
`;
|
||||
|
||||
interface ExceptionsFlyoutAlertsActionsComponentProps {
|
||||
exceptionListItems: ExceptionsBuilderReturnExceptionItem[];
|
||||
exceptionListType: ExceptionListType;
|
||||
shouldBulkCloseAlert: boolean;
|
||||
disableBulkClose: boolean;
|
||||
alertData?: AlertData;
|
||||
alertStatus?: Status;
|
||||
isAlertDataLoading?: boolean;
|
||||
shouldCloseSingleAlert?: boolean;
|
||||
onUpdateBulkCloseIndex: (arg: string[] | undefined) => void;
|
||||
onBulkCloseCheckboxChange: (arg: boolean) => void;
|
||||
onSingleAlertCloseCheckboxChange?: (arg: boolean) => void;
|
||||
onDisableBulkClose: (arg: boolean) => void;
|
||||
}
|
||||
|
||||
const ExceptionItemsFlyoutAlertsActionsComponent: React.FC<
|
||||
ExceptionsFlyoutAlertsActionsComponentProps
|
||||
> = ({
|
||||
isAlertDataLoading,
|
||||
exceptionListItems,
|
||||
exceptionListType,
|
||||
shouldCloseSingleAlert,
|
||||
shouldBulkCloseAlert,
|
||||
disableBulkClose,
|
||||
alertData,
|
||||
alertStatus,
|
||||
onDisableBulkClose,
|
||||
onUpdateBulkCloseIndex,
|
||||
onBulkCloseCheckboxChange,
|
||||
onSingleAlertCloseCheckboxChange,
|
||||
}): JSX.Element => {
|
||||
const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex();
|
||||
const memoSignalIndexName = useMemo(
|
||||
() => (signalIndexName !== null ? [signalIndexName] : []),
|
||||
[signalIndexName]
|
||||
);
|
||||
const [isSignalIndexPatternLoading, { indexPatterns: signalIndexPatterns }] =
|
||||
useFetchIndex(memoSignalIndexName);
|
||||
|
||||
const handleBulkCloseCheckbox = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
onBulkCloseCheckboxChange(event.currentTarget.checked);
|
||||
},
|
||||
[onBulkCloseCheckboxChange]
|
||||
);
|
||||
|
||||
const handleCloseSingleAlertCheckbox = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
if (onSingleAlertCloseCheckboxChange != null) {
|
||||
onSingleAlertCloseCheckboxChange(event.currentTarget.checked);
|
||||
}
|
||||
},
|
||||
[onSingleAlertCloseCheckboxChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onUpdateBulkCloseIndex(
|
||||
shouldBulkCloseAlert && memoSignalIndexName != null ? memoSignalIndexName : undefined
|
||||
);
|
||||
}, [memoSignalIndexName, onUpdateBulkCloseIndex, shouldBulkCloseAlert]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (disableBulkClose === true) {
|
||||
onBulkCloseCheckboxChange(false);
|
||||
}
|
||||
}, [disableBulkClose, onBulkCloseCheckboxChange]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (isSignalIndexPatternLoading === false && isSignalIndexLoading === false) {
|
||||
onDisableBulkClose(
|
||||
entryHasListType(exceptionListItems) ||
|
||||
entryHasNonEcsType(exceptionListItems, signalIndexPatterns) ||
|
||||
exceptionListItems.every((item) => item.entries.length === 0)
|
||||
);
|
||||
}
|
||||
}, [
|
||||
onDisableBulkClose,
|
||||
exceptionListItems,
|
||||
isSignalIndexPatternLoading,
|
||||
isSignalIndexLoading,
|
||||
signalIndexPatterns,
|
||||
]);
|
||||
|
||||
return (
|
||||
<FlyoutCheckboxesSection>
|
||||
<SectionHeader size="xs">
|
||||
<h3>{i18n.CLOSE_ALERTS_SECTION_TITLE}</h3>
|
||||
</SectionHeader>
|
||||
<EuiSpacer size="s" />
|
||||
{alertData != null && alertStatus !== 'closed' && (
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiCheckbox
|
||||
data-test-subj="closeAlertOnAddExceptionCheckbox"
|
||||
id="close-alert-on-add-add-exception-checkbox"
|
||||
label={i18n.SINGLE_ALERT_CLOSE_LABEL}
|
||||
checked={shouldCloseSingleAlert}
|
||||
onChange={handleCloseSingleAlertCheckbox}
|
||||
disabled={isSignalIndexLoading || isSignalIndexPatternLoading || isAlertDataLoading}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiCheckbox
|
||||
data-test-subj="bulkCloseAlertOnAddExceptionCheckbox"
|
||||
id="bulk-close-alert-on-add-add-exception-checkbox"
|
||||
label={disableBulkClose ? i18n.BULK_CLOSE_LABEL_DISABLED : i18n.BULK_CLOSE_LABEL}
|
||||
checked={shouldBulkCloseAlert}
|
||||
onChange={handleBulkCloseCheckbox}
|
||||
disabled={
|
||||
disableBulkClose ||
|
||||
isSignalIndexLoading ||
|
||||
isSignalIndexPatternLoading ||
|
||||
isAlertDataLoading
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{exceptionListType === 'endpoint' && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText data-test-subj="addExceptionEndpointText" color="subdued" size="s">
|
||||
{i18n.ENDPOINT_QUARANTINE_TEXT}
|
||||
</EuiText>
|
||||
</>
|
||||
)}
|
||||
</FlyoutCheckboxesSection>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExceptionItemsFlyoutAlertsActions = React.memo(
|
||||
ExceptionItemsFlyoutAlertsActionsComponent
|
||||
);
|
||||
|
||||
ExceptionItemsFlyoutAlertsActions.displayName = 'ExceptionItemsFlyoutAlertsActions';
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const CLOSE_ALERTS_SECTION_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.alertsActions.sectionTitle',
|
||||
{
|
||||
defaultMessage: 'Alerts actions',
|
||||
}
|
||||
);
|
||||
|
||||
export const SINGLE_ALERT_CLOSE_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.alertsActions.singleAlertCloseLabel',
|
||||
{
|
||||
defaultMessage: 'Close this alert',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_CLOSE_LABEL_DISABLED = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.alertsActions.bulkCloseLabel.disabled',
|
||||
{
|
||||
defaultMessage:
|
||||
'Close all alerts that match this exception and were generated by this rule (Lists and non-ECS fields are not supported)',
|
||||
}
|
||||
);
|
||||
|
||||
export const BULK_CLOSE_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.alertsActions.bulkCloseLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Close all alerts that match this exception and were generated by selected rule/s',
|
||||
}
|
||||
);
|
||||
|
||||
export const ENDPOINT_QUARANTINE_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.alertsActions.endpointQuarantineText',
|
||||
{
|
||||
defaultMessage:
|
||||
'On all Endpoint hosts, quarantined files that match the exception are automatically restored to their original locations. This exception applies to all rules using Endpoint exceptions.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
|
||||
import type { EntriesArray, ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { entryHasNonEcsType, entryHasListType } from './utils';
|
||||
import type { DataViewBase } from '@kbn/es-query';
|
||||
|
||||
describe('alerts_actions#utils', () => {
|
||||
describe('#entryHasListType', () => {
|
||||
test('it should return false with an empty array', () => {
|
||||
const payload: ExceptionListItemSchema[] = [];
|
||||
const result = entryHasListType(payload);
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
test("it should return false with exception items that don't contain a list type", () => {
|
||||
const payload = [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()];
|
||||
const result = entryHasListType(payload);
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
test('it should return true with exception items that do contain a list type', () => {
|
||||
const payload = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [{ type: ListOperatorTypeEnum.LIST }] as EntriesArray,
|
||||
},
|
||||
getExceptionListItemSchemaMock(),
|
||||
];
|
||||
const result = entryHasListType(payload);
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#entryHasNonEcsType', () => {
|
||||
const mockEcsIndexPattern = {
|
||||
title: 'testIndex',
|
||||
fields: [
|
||||
{
|
||||
name: 'some.parentField',
|
||||
},
|
||||
{
|
||||
name: 'some.not.nested.field',
|
||||
},
|
||||
{
|
||||
name: 'nested.field',
|
||||
},
|
||||
],
|
||||
} as DataViewBase;
|
||||
|
||||
test('it should return false with an empty array', () => {
|
||||
const payload: ExceptionListItemSchema[] = [];
|
||||
const result = entryHasNonEcsType(payload, mockEcsIndexPattern);
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
test("it should return false with exception items that don't contain a non ecs type", () => {
|
||||
const payload = [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()];
|
||||
const result = entryHasNonEcsType(payload, mockEcsIndexPattern);
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
test('it should return true with exception items that do contain a non ecs type', () => {
|
||||
const payload = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
entries: [{ field: 'some.nonEcsField' }] as EntriesArray,
|
||||
},
|
||||
getExceptionListItemSchemaMock(),
|
||||
];
|
||||
const result = entryHasNonEcsType(payload, mockEcsIndexPattern);
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { DataViewBase } from '@kbn/es-query';
|
||||
import type { Entry } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type {
|
||||
EmptyEntry,
|
||||
EmptyListEntry,
|
||||
ExceptionsBuilderReturnExceptionItem,
|
||||
} from '@kbn/securitysolution-list-utils';
|
||||
import { getOperatorType } from '@kbn/securitysolution-list-utils';
|
||||
|
||||
/**
|
||||
* Determines if item entries has 'is in list'/'is not in list' entry
|
||||
*/
|
||||
export const entryHasListType = (exceptionItems: ExceptionsBuilderReturnExceptionItem[]) => {
|
||||
for (const { entries } of exceptionItems) {
|
||||
for (const exceptionEntry of entries ?? []) {
|
||||
if (getOperatorType(exceptionEntry) === ListOperatorTypeEnum.LIST) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether or not any entries within the given exceptionItems contain values not in the specified ECS mapping
|
||||
*/
|
||||
export const entryHasNonEcsType = (
|
||||
exceptionItems: ExceptionsBuilderReturnExceptionItem[],
|
||||
indexPatterns: DataViewBase
|
||||
): boolean => {
|
||||
const doesFieldNameExist = (exceptionEntry: Entry | EmptyListEntry | EmptyEntry): boolean => {
|
||||
return indexPatterns.fields.some(({ name }) => name === exceptionEntry.field);
|
||||
};
|
||||
|
||||
if (exceptionItems.length === 0) {
|
||||
return false;
|
||||
}
|
||||
for (const { entries } of exceptionItems) {
|
||||
for (const exceptionEntry of entries ?? []) {
|
||||
if (exceptionEntry.type === 'nested') {
|
||||
for (const nestedExceptionEntry of exceptionEntry.entries) {
|
||||
if (doesFieldNameExist(nestedExceptionEntry) === false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (doesFieldNameExist(exceptionEntry) === false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { ExceptionsConditions } from '.';
|
||||
import { TestProviders, mockIndexPattern } from '../../../../../common/mock';
|
||||
import { getRulesEqlSchemaMock } from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
|
||||
import type { Rule } from '../../../../../detections/containers/detection_engine/rules/types';
|
||||
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
jest.mock('@kbn/lists-plugin/public');
|
||||
|
||||
describe('ExceptionsConditions', () => {
|
||||
describe('EQL rule type', () => {
|
||||
it('it displays EQL warning callout if rule is EQL sequence', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionsConditions
|
||||
exceptionItemName={'Item name'}
|
||||
allowLargeValueLists={false}
|
||||
exceptionListItems={[]}
|
||||
indexPatterns={mockIndexPattern}
|
||||
rules={[
|
||||
{
|
||||
...getRulesEqlSchemaMock(),
|
||||
query: 'sequence [process where process.name = "test.exe"]',
|
||||
} as Rule,
|
||||
]}
|
||||
showOsTypeOptions={false}
|
||||
isEdit={false}
|
||||
selectedOs={undefined}
|
||||
exceptionListType={ExceptionListTypeEnum.DETECTION}
|
||||
onOsChange={jest.fn()}
|
||||
onExceptionItemAdd={jest.fn()}
|
||||
onSetErrorExists={jest.fn()}
|
||||
onFilterIndexPatterns={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="eqlSequenceCallout"]').at(0).text()).toEqual(
|
||||
i18n.ADD_EXCEPTION_SEQUENCE_WARNING
|
||||
);
|
||||
});
|
||||
|
||||
it('it displays EQL editing warning callout if rule is EQL sequence and "isEdit" is "true"', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionsConditions
|
||||
exceptionItemName={'Item name'}
|
||||
allowLargeValueLists={false}
|
||||
exceptionListItems={[getExceptionListItemSchemaMock()]}
|
||||
indexPatterns={mockIndexPattern}
|
||||
rules={[
|
||||
{
|
||||
...getRulesEqlSchemaMock(),
|
||||
query: 'sequence [process where process.name = "test.exe"]',
|
||||
} as Rule,
|
||||
]}
|
||||
showOsTypeOptions={false}
|
||||
isEdit
|
||||
selectedOs={undefined}
|
||||
exceptionListType={ExceptionListTypeEnum.RULE_DEFAULT}
|
||||
onOsChange={jest.fn()}
|
||||
onExceptionItemAdd={jest.fn()}
|
||||
onSetErrorExists={jest.fn()}
|
||||
onFilterIndexPatterns={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="eqlSequenceCallout"]').at(0).text()).toEqual(
|
||||
i18n.EDIT_EXCEPTION_SEQUENCE_WARNING
|
||||
);
|
||||
});
|
||||
|
||||
it('it does not display EQL warning callout if rule is EQL sequence', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionsConditions
|
||||
exceptionItemName={'Item name'}
|
||||
allowLargeValueLists={false}
|
||||
exceptionListItems={[]}
|
||||
indexPatterns={mockIndexPattern}
|
||||
rules={[
|
||||
{
|
||||
...getRulesEqlSchemaMock(),
|
||||
} as Rule,
|
||||
]}
|
||||
showOsTypeOptions={false}
|
||||
isEdit={false}
|
||||
selectedOs={undefined}
|
||||
exceptionListType={ExceptionListTypeEnum.DETECTION}
|
||||
onOsChange={jest.fn()}
|
||||
onExceptionItemAdd={jest.fn()}
|
||||
onSetErrorExists={jest.fn()}
|
||||
onFilterIndexPatterns={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="eqlSequenceCallout"]').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('OS options', () => {
|
||||
it('it displays os options if "showOsTypeOptions" is "true"', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionsConditions
|
||||
exceptionItemName={'Item name'}
|
||||
allowLargeValueLists={false}
|
||||
exceptionListItems={[]}
|
||||
indexPatterns={mockIndexPattern}
|
||||
rules={[
|
||||
{
|
||||
...getRulesEqlSchemaMock(),
|
||||
} as Rule,
|
||||
]}
|
||||
showOsTypeOptions
|
||||
isEdit={false}
|
||||
selectedOs={undefined}
|
||||
exceptionListType={ExceptionListTypeEnum.ENDPOINT}
|
||||
onOsChange={jest.fn()}
|
||||
onExceptionItemAdd={jest.fn()}
|
||||
onSetErrorExists={jest.fn()}
|
||||
onFilterIndexPatterns={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="osSelectionDropdown"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it displays the exception item os text if "isEdit" is "true"', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionsConditions
|
||||
exceptionItemName={'Item name'}
|
||||
allowLargeValueLists={false}
|
||||
exceptionListItems={[
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
os_types: ['windows'],
|
||||
},
|
||||
]}
|
||||
indexPatterns={mockIndexPattern}
|
||||
rules={[
|
||||
{
|
||||
...getRulesEqlSchemaMock(),
|
||||
} as Rule,
|
||||
]}
|
||||
showOsTypeOptions
|
||||
isEdit
|
||||
exceptionListType={ExceptionListTypeEnum.ENDPOINT}
|
||||
selectedOs={undefined}
|
||||
onOsChange={jest.fn()}
|
||||
onExceptionItemAdd={jest.fn()}
|
||||
onSetErrorExists={jest.fn()}
|
||||
onFilterIndexPatterns={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
// Text appears funky since not applying styling
|
||||
expect(wrapper.find('[data-test-subj="exceptionItemSelectedOs"]').at(0).text()).toEqual(
|
||||
'Operating SystemWindows'
|
||||
);
|
||||
});
|
||||
|
||||
it('it does not display os options if "showOsTypeOptions" is "false"', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionsConditions
|
||||
exceptionItemName={'Item name'}
|
||||
allowLargeValueLists={false}
|
||||
exceptionListItems={[]}
|
||||
indexPatterns={mockIndexPattern}
|
||||
rules={[
|
||||
{
|
||||
...getRulesEqlSchemaMock(),
|
||||
} as Rule,
|
||||
]}
|
||||
showOsTypeOptions={false}
|
||||
isEdit={false}
|
||||
selectedOs={undefined}
|
||||
exceptionListType={ExceptionListTypeEnum.ENDPOINT}
|
||||
onOsChange={jest.fn()}
|
||||
onExceptionItemAdd={jest.fn()}
|
||||
onSetErrorExists={jest.fn()}
|
||||
onFilterIndexPatterns={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="osSelectionDropdown"]').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,276 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo } from 'react';
|
||||
import type { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { EuiCallOut, EuiComboBox, EuiFormRow, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public';
|
||||
import type {
|
||||
CreateRuleExceptionListItemSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
ExceptionListItemSchema,
|
||||
ExceptionListType,
|
||||
OsType,
|
||||
OsTypeArray,
|
||||
NamespaceType,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type {
|
||||
ExceptionsBuilderExceptionItem,
|
||||
ExceptionsBuilderReturnExceptionItem,
|
||||
} from '@kbn/securitysolution-list-utils';
|
||||
import type { DataViewBase } from '@kbn/es-query';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants';
|
||||
import { hasEqlSequenceQuery, isEqlRule } from '../../../../../../common/detection_engine/utils';
|
||||
import type { Rule } from '../../../../../detections/containers/detection_engine/rules/types';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import * as i18n from './translations';
|
||||
import * as sharedI18n from '../../../utils/translations';
|
||||
|
||||
const OS_OPTIONS: Array<EuiComboBoxOptionOption<OsTypeArray>> = [
|
||||
{
|
||||
label: sharedI18n.OPERATING_SYSTEM_WINDOWS,
|
||||
value: ['windows'],
|
||||
},
|
||||
{
|
||||
label: sharedI18n.OPERATING_SYSTEM_MAC,
|
||||
value: ['macos'],
|
||||
},
|
||||
{
|
||||
label: sharedI18n.OPERATING_SYSTEM_LINUX,
|
||||
value: ['linux'],
|
||||
},
|
||||
{
|
||||
label: sharedI18n.OPERATING_SYSTEM_WINDOWS_AND_MAC,
|
||||
value: ['windows', 'macos'],
|
||||
},
|
||||
];
|
||||
|
||||
const SectionHeader = styled(EuiTitle)`
|
||||
${() => css`
|
||||
font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
|
||||
`}
|
||||
`;
|
||||
|
||||
interface ExceptionsFlyoutConditionsComponentProps {
|
||||
/* Exception list item field value for "name" */
|
||||
exceptionItemName: string;
|
||||
/* Not all rule types support large value lists */
|
||||
allowLargeValueLists: boolean;
|
||||
/* Exception items - could be one being edited, or multiple being added */
|
||||
exceptionListItems: ExceptionsBuilderExceptionItem[];
|
||||
/* Fields used to populate the field option dropdown */
|
||||
indexPatterns: DataViewBase;
|
||||
/* Exception items can be added to zero (just being added to a shared list), one or more rules */
|
||||
rules: Rule[] | null;
|
||||
/* OS options required for endpoint exceptions */
|
||||
showOsTypeOptions: boolean;
|
||||
/* Selected OS option required for endpoint exceptions */
|
||||
selectedOs: OsTypeArray | undefined;
|
||||
/* Determines whether component is being used in an add or edit functionality */
|
||||
isEdit: boolean;
|
||||
/*
|
||||
* Supported exception list types are 'endpoint', 'detection' and 'rule_default' */
|
||||
exceptionListType: ExceptionListType;
|
||||
/* OS selection handler */
|
||||
onOsChange?: (os: OsTypeArray | undefined) => void;
|
||||
/* Exception item builder takes a callback used when there are updates to the item */
|
||||
|
||||
onExceptionItemAdd: (items: ExceptionsBuilderReturnExceptionItem[]) => void;
|
||||
/* Exception item builder takes a callback used when there are updates to the item that includes information on if any form errors exist */
|
||||
onSetErrorExists: (errorExists: boolean) => void;
|
||||
onFilterIndexPatterns: (
|
||||
patterns: DataViewBase,
|
||||
type: ExceptionListType,
|
||||
osTypes?: Array<'linux' | 'macos' | 'windows'> | undefined
|
||||
) => DataViewBase;
|
||||
}
|
||||
|
||||
const ExceptionsConditionsComponent: React.FC<ExceptionsFlyoutConditionsComponentProps> = ({
|
||||
exceptionItemName,
|
||||
allowLargeValueLists,
|
||||
exceptionListItems,
|
||||
indexPatterns,
|
||||
rules,
|
||||
exceptionListType,
|
||||
showOsTypeOptions,
|
||||
selectedOs,
|
||||
isEdit,
|
||||
onOsChange,
|
||||
onExceptionItemAdd,
|
||||
onSetErrorExists,
|
||||
onFilterIndexPatterns,
|
||||
}): JSX.Element => {
|
||||
const { http, unifiedSearch } = useKibana().services;
|
||||
const isEndpointException = useMemo(
|
||||
(): boolean => exceptionListType === ExceptionListTypeEnum.ENDPOINT,
|
||||
[exceptionListType]
|
||||
);
|
||||
const includesRuleWithEQLSequenceStatement = useMemo((): boolean => {
|
||||
return (
|
||||
rules != null && rules.some((rule) => isEqlRule(rule.type) && hasEqlSequenceQuery(rule.query))
|
||||
);
|
||||
}, [rules]);
|
||||
|
||||
// If editing an item (can only edit a single item at a time), get it's
|
||||
// list_id. Otherwise, if it is an item for an endpoint list, the list_id is
|
||||
// hard coded. For all others, we'll fill in the appropriate list_id when
|
||||
// enriching items before creating when we know if they'll be added to the rule_default
|
||||
// list or a shared list.
|
||||
const listIdToUse = useMemo((): string | undefined => {
|
||||
if (isEndpointException) {
|
||||
return 'endpoint_list';
|
||||
}
|
||||
|
||||
const defaultValue = isEndpointException ? ENDPOINT_LIST_ID : undefined;
|
||||
|
||||
return isEdit ? exceptionListItems[0].list_id : defaultValue;
|
||||
}, [isEndpointException, isEdit, exceptionListItems]);
|
||||
|
||||
// If editing an item (can only edit a single item at a time), get it's
|
||||
// namespace_type. Otherwise, if it is an item for an endpoint list, the namespace_type is
|
||||
// 'agnostic'. For all others, we'll fill in the appropriate list_id when
|
||||
// enriching items before creating when we know if they'll be added to the rule_default
|
||||
// list or a shared list.
|
||||
const listNamespaceType = useMemo((): NamespaceType | undefined => {
|
||||
const defaultValue = isEndpointException ? 'agnostic' : undefined;
|
||||
|
||||
return isEdit ? exceptionListItems[0].namespace_type : defaultValue;
|
||||
}, [exceptionListItems, isEdit, isEndpointException]);
|
||||
|
||||
const handleBuilderOnChange = useCallback(
|
||||
({
|
||||
exceptionItems,
|
||||
errorExists,
|
||||
}: {
|
||||
exceptionItems: Array<
|
||||
ExceptionListItemSchema | CreateExceptionListItemSchema | CreateRuleExceptionListItemSchema
|
||||
>;
|
||||
errorExists: boolean;
|
||||
}) => {
|
||||
onExceptionItemAdd(exceptionItems);
|
||||
onSetErrorExists(errorExists);
|
||||
},
|
||||
[onSetErrorExists, onExceptionItemAdd]
|
||||
);
|
||||
|
||||
const handleOSSelectionChange = useCallback(
|
||||
(selectedOptions: Array<EuiComboBoxOptionOption<OsTypeArray>>): void => {
|
||||
const os = selectedOptions[0].value;
|
||||
if (onOsChange != null) {
|
||||
onOsChange(os ? os : undefined);
|
||||
}
|
||||
},
|
||||
[onOsChange]
|
||||
);
|
||||
|
||||
const osSingleSelectionOptions = useMemo(() => {
|
||||
return { asPlainText: true };
|
||||
}, []);
|
||||
|
||||
const selectedOStoOptions = useMemo((): Array<EuiComboBoxOptionOption<OsTypeArray>> => {
|
||||
return OS_OPTIONS.filter((option) => {
|
||||
return selectedOs === option.value;
|
||||
});
|
||||
}, [selectedOs]);
|
||||
|
||||
const isExceptionBuilderFormDisabled = useMemo(() => {
|
||||
return showOsTypeOptions && selectedOs === undefined;
|
||||
}, [showOsTypeOptions, selectedOs]);
|
||||
|
||||
const osDisplay = (osTypes: OsTypeArray): string => {
|
||||
const translateOS = (currentOs: OsType): string => {
|
||||
return currentOs === 'linux'
|
||||
? sharedI18n.OPERATING_SYSTEM_LINUX
|
||||
: currentOs === 'macos'
|
||||
? sharedI18n.OPERATING_SYSTEM_MAC
|
||||
: sharedI18n.OPERATING_SYSTEM_WINDOWS;
|
||||
};
|
||||
return osTypes
|
||||
.reduce((osString, currentOs) => {
|
||||
return `${translateOS(currentOs)}, ${osString}`;
|
||||
}, '')
|
||||
.slice(0, -2);
|
||||
};
|
||||
|
||||
const eqlCalloutWarning = useMemo((): string => {
|
||||
return isEdit ? i18n.EDIT_EXCEPTION_SEQUENCE_WARNING : i18n.ADD_EXCEPTION_SEQUENCE_WARNING;
|
||||
}, [isEdit]);
|
||||
|
||||
const osTypes = useMemo((): OsTypeArray => {
|
||||
return (isEdit ? exceptionListItems[0].os_types : selectedOs) ?? [];
|
||||
}, [exceptionListItems, isEdit, selectedOs]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader size="xs">
|
||||
<h3>{i18n.RULE_EXCEPTION_CONDITIONS}</h3>
|
||||
</SectionHeader>
|
||||
{includesRuleWithEQLSequenceStatement && (
|
||||
<>
|
||||
<EuiCallOut data-test-subj="eqlSequenceCallout" title={eqlCalloutWarning} />
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s">{i18n.EXCEPTION_BUILDER_INFO}</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
{showOsTypeOptions && !isEdit && (
|
||||
<>
|
||||
<EuiFormRow label={sharedI18n.OPERATING_SYSTEM_LABEL}>
|
||||
<EuiComboBox
|
||||
placeholder={i18n.OPERATING_SYSTEM_PLACEHOLDER}
|
||||
singleSelection={osSingleSelectionOptions}
|
||||
options={OS_OPTIONS}
|
||||
selectedOptions={selectedOStoOptions}
|
||||
onChange={handleOSSelectionChange}
|
||||
isClearable={false}
|
||||
data-test-subj="osSelectionDropdown"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="l" />
|
||||
</>
|
||||
)}
|
||||
{showOsTypeOptions && isEdit && (
|
||||
<>
|
||||
<EuiText size="xs" data-test-subj="exceptionItemSelectedOs">
|
||||
<dl>
|
||||
<dt>{sharedI18n.OPERATING_SYSTEM_LABEL}</dt>
|
||||
<dd>{osDisplay(osTypes)}</dd>
|
||||
</dl>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
{getExceptionBuilderComponentLazy({
|
||||
allowLargeValueLists,
|
||||
httpService: http,
|
||||
autocompleteService: unifiedSearch.autocomplete,
|
||||
exceptionListItems,
|
||||
listType: exceptionListType,
|
||||
osTypes,
|
||||
listId: listIdToUse,
|
||||
listNamespaceType,
|
||||
listTypeSpecificIndexPatternFilter: onFilterIndexPatterns,
|
||||
exceptionItemName,
|
||||
indexPatterns,
|
||||
isOrDisabled: isExceptionBuilderFormDisabled,
|
||||
isAndDisabled: isExceptionBuilderFormDisabled,
|
||||
isNestedDisabled: isExceptionBuilderFormDisabled,
|
||||
dataTestSubj: 'alertExceptionBuilder',
|
||||
idAria: 'alertExceptionBuilder',
|
||||
onChange: handleBuilderOnChange,
|
||||
isDisabled: isExceptionBuilderFormDisabled,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExceptionsConditions = React.memo(ExceptionsConditionsComponent);
|
||||
|
||||
ExceptionsConditions.displayName = 'ExceptionsConditions';
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
// Conditions component
|
||||
export const RULE_EXCEPTION_CONDITIONS = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.itemConditions.conditionsTitle',
|
||||
{
|
||||
defaultMessage: 'Conditions',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXCEPTION_BUILDER_INFO = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.itemConditions.infoLabel',
|
||||
{
|
||||
defaultMessage: "Alerts are generated when the rule's conditions are met, except when:",
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_EXCEPTION_SEQUENCE_WARNING = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.itemConditions.sequenceWarningAdd',
|
||||
{
|
||||
defaultMessage:
|
||||
"This rule's query contains an EQL sequence statement. The exception created will apply to all events in the sequence.",
|
||||
}
|
||||
);
|
||||
|
||||
export const EDIT_EXCEPTION_SEQUENCE_WARNING = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.itemConditions.sequenceWarningEdit',
|
||||
{
|
||||
defaultMessage:
|
||||
"This rule's query contains an EQL sequence statement. The exception modified will apply to all events in the sequence.",
|
||||
}
|
||||
);
|
||||
|
||||
export const OPERATING_SYSTEM_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.itemConditions.operatingSystemPlaceHolder',
|
||||
{
|
||||
defaultMessage: 'Select an operating system',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { EuiFieldText } from '@elastic/eui';
|
||||
|
||||
import { ExceptionsFlyoutMeta } from '.';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
|
||||
describe('ExceptionsFlyoutMeta', () => {
|
||||
it('it renders component', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionsFlyoutMeta exceptionItemName={'Test name'} onChange={jest.fn()} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionFlyoutName"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="exceptionFlyoutNameInput"]').at(1).props().value).toEqual(
|
||||
'Test name'
|
||||
);
|
||||
});
|
||||
|
||||
it('it calls onChange on name change', () => {
|
||||
const mockOnChange = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionsFlyoutMeta exceptionItemName={''} onChange={mockOnChange} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
(
|
||||
wrapper.find(EuiFieldText).at(0).props() as unknown as {
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
).onChange({ target: { value: 'Name change' } } as React.ChangeEvent<HTMLInputElement>);
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(['name', 'Name change']);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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, { useCallback } from 'react';
|
||||
import { EuiFormRow, EuiFieldText } from '@elastic/eui';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface ExceptionsFlyoutMetaComponentProps {
|
||||
exceptionItemName: string;
|
||||
onChange: (value: [string, string]) => void;
|
||||
}
|
||||
|
||||
const ExceptionsFlyoutMetaComponent: React.FC<ExceptionsFlyoutMetaComponentProps> = ({
|
||||
exceptionItemName,
|
||||
onChange,
|
||||
}): JSX.Element => {
|
||||
const onNameChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(['name', e.target.value]);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow label={i18n.RULE_EXCEPTION_NAME_LABEL} data-test-subj="exceptionFlyoutName">
|
||||
<EuiFieldText
|
||||
placeholder={i18n.RULE_EXCEPTION_NAME_PLACEHOLDER}
|
||||
value={exceptionItemName}
|
||||
onChange={onNameChange}
|
||||
data-test-subj="exceptionFlyoutNameInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExceptionsFlyoutMeta = React.memo(ExceptionsFlyoutMetaComponent);
|
||||
|
||||
ExceptionsFlyoutMeta.displayName = 'ExceptionsFlyoutMeta';
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 RULE_EXCEPTION_NAME_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.itemMeta.nameLabel',
|
||||
{
|
||||
defaultMessage: 'Rule exception name',
|
||||
}
|
||||
);
|
||||
|
||||
export const RULE_EXCEPTION_NAME_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.itemMeta.namePlaceholder',
|
||||
{
|
||||
defaultMessage: 'Name your rule exception',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { ExceptionsLinkedToLists } from '.';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { mount } from 'enzyme';
|
||||
import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock';
|
||||
|
||||
jest.mock('../../../logic/use_find_references');
|
||||
|
||||
describe('ExceptionsLinkedToLists', () => {
|
||||
it('it displays loading state while "isLoadingReferences" is "true"', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ExceptionsLinkedToLists
|
||||
isLoadingReferences={true}
|
||||
errorFetchingReferences={false}
|
||||
listAndReferences={[]}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionItemListsTableLoading"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('it displays error state if "errorFetchingReferences" is "true"', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ExceptionsLinkedToLists
|
||||
isLoadingReferences={false}
|
||||
errorFetchingReferences
|
||||
listAndReferences={[]}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('EuiInMemoryTable').prop('error')).toEqual(
|
||||
'Unable to fetch exception list.'
|
||||
);
|
||||
});
|
||||
|
||||
it('it displays lists with rule references', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ExceptionsLinkedToLists
|
||||
isLoadingReferences={false}
|
||||
errorFetchingReferences={false}
|
||||
listAndReferences={[
|
||||
{
|
||||
...getExceptionListSchemaMock(),
|
||||
id: '123',
|
||||
list_id: 'my_list_id',
|
||||
namespace_type: 'single',
|
||||
type: ExceptionListTypeEnum.DETECTION,
|
||||
name: 'My exception list',
|
||||
referenced_rules: [
|
||||
{
|
||||
id: '345',
|
||||
name: 'My rule',
|
||||
rule_id: 'my_rule_id',
|
||||
exception_lists: [
|
||||
{
|
||||
id: '123',
|
||||
list_id: 'my_list_id',
|
||||
namespace_type: 'single',
|
||||
type: ExceptionListTypeEnum.DETECTION,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="ruleReferencesDisplayPopoverButton"]').at(1).text()
|
||||
).toEqual('1');
|
||||
// Formatting is off since doesn't take css into account
|
||||
expect(wrapper.find('[data-test-subj="exceptionListNameCell"]').at(1).text()).toEqual(
|
||||
'NameMy exception list'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { EuiTitle, EuiSpacer, EuiPanel, EuiInMemoryTable, EuiLoadingContent } from '@elastic/eui';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import type { ExceptionListRuleReferencesSchema } from '../../../../../../common/detection_engine/schemas/response';
|
||||
import { getSharedListsTableColumns } from '../utils';
|
||||
|
||||
interface ExceptionsLinkedToListComponentProps {
|
||||
isLoadingReferences: boolean;
|
||||
errorFetchingReferences: boolean;
|
||||
listAndReferences: ExceptionListRuleReferencesSchema[];
|
||||
}
|
||||
|
||||
const SectionHeader = styled(EuiTitle)`
|
||||
${() => css`
|
||||
font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
|
||||
`}
|
||||
`;
|
||||
|
||||
const ExceptionsLinkedToListsComponent: React.FC<ExceptionsLinkedToListComponentProps> = ({
|
||||
isLoadingReferences,
|
||||
errorFetchingReferences,
|
||||
listAndReferences,
|
||||
}): JSX.Element => {
|
||||
const [message, setMessage] = useState<JSX.Element | string | undefined>(
|
||||
<EuiLoadingContent lines={4} data-test-subj="exceptionItemListsTableLoading" />
|
||||
);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (errorFetchingReferences) {
|
||||
setError(i18n.LINKED_TO_LIST_ERROR);
|
||||
} else if (!isLoadingReferences) {
|
||||
setMessage(undefined);
|
||||
}
|
||||
}, [errorFetchingReferences, isLoadingReferences]);
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
paddingSize="none"
|
||||
hasShadow={false}
|
||||
data-test-subj="exceptionItemLinkedToListSection"
|
||||
>
|
||||
<SectionHeader size="xs">
|
||||
<h3>{i18n.LINKED_TO_LIST_TITLE}</h3>
|
||||
</SectionHeader>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiInMemoryTable<ExceptionListRuleReferencesSchema>
|
||||
tableCaption="Table of exception lists"
|
||||
itemId="id"
|
||||
message={message}
|
||||
loading={isLoadingReferences}
|
||||
items={listAndReferences}
|
||||
error={error}
|
||||
columns={getSharedListsTableColumns()}
|
||||
isSelectable={false}
|
||||
sorting
|
||||
data-test-subj="exceptionItemSharedList"
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExceptionsLinkedToLists = React.memo(ExceptionsLinkedToListsComponent);
|
||||
|
||||
ExceptionsLinkedToLists.displayName = 'ExceptionsLinkedToLists';
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 LINKED_TO_LIST_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.linkedToListSection.title',
|
||||
{
|
||||
defaultMessage: 'Linked to shared list',
|
||||
}
|
||||
);
|
||||
|
||||
export const LINKED_TO_LIST_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.linkedToListSection.error',
|
||||
{
|
||||
defaultMessage: 'Unable to fetch exception list.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { ExceptionsLinkedToRule } from '.';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { getRulesSchemaMock } from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
|
||||
import type { Rule } from '../../../../../detections/containers/detection_engine/rules/types';
|
||||
|
||||
jest.mock('../../../../../detections/pages/detection_engine/rules/all/rules_table/use_find_rules');
|
||||
|
||||
describe('ExceptionsLinkedToRule', () => {
|
||||
it('it displays rule name and link', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionsLinkedToRule
|
||||
rule={{ ...getRulesSchemaMock(), id: '345', name: 'My rule' } as Rule}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="ruleNameCell"]').at(0).text()).toEqual('NameMy rule');
|
||||
expect(wrapper.find('[data-test-subj="ruleAction-viewDetails"]').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { EuiTitle, EuiSpacer, EuiInMemoryTable } from '@elastic/eui';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import type { Rule } from '../../../../../detections/containers/detection_engine/rules/types';
|
||||
import { getRulesTableColumn } from '../utils';
|
||||
|
||||
interface ExceptionsLinkedToRuleComponentProps {
|
||||
rule: Rule;
|
||||
}
|
||||
|
||||
const SectionHeader = styled(EuiTitle)`
|
||||
${() => css`
|
||||
font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
|
||||
`}
|
||||
`;
|
||||
|
||||
const ExceptionsLinkedToRuleComponent: React.FC<ExceptionsLinkedToRuleComponentProps> = ({
|
||||
rule,
|
||||
}): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<SectionHeader size="xs" data-test-subj="exceptionItemLinkedToRuleSection">
|
||||
<h3>{i18n.LINKED_TO_RULE_TITLE}</h3>
|
||||
</SectionHeader>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiInMemoryTable<Rule>
|
||||
tableCaption="Rules table"
|
||||
itemId="id"
|
||||
items={[rule]}
|
||||
columns={getRulesTableColumn()}
|
||||
sorting
|
||||
data-test-subj="addExceptionToRulesTable"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExceptionsLinkedToRule = React.memo(ExceptionsLinkedToRuleComponent);
|
||||
|
||||
ExceptionsLinkedToRule.displayName = 'ExceptionsLinkedToRule';
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 LINKED_TO_RULE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.linkedToRule.title',
|
||||
{
|
||||
defaultMessage: 'Linked to rule',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 VIEW_LIST_DETAIL_ACTION = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.viewListDetailActionLabel',
|
||||
{
|
||||
defaultMessage: 'View list detail',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_RULE_DETAIL_ACTION = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.flyoutComponents.viewRuleDetailActionLabel',
|
||||
{
|
||||
defaultMessage: 'View rule detail',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,284 @@
|
|||
/*
|
||||
* 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 { getCreateExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock';
|
||||
import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock';
|
||||
import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type { ExceptionsBuilderReturnExceptionItem } from '@kbn/securitysolution-list-utils';
|
||||
|
||||
import {
|
||||
enrichItemWithComment,
|
||||
enrichItemWithName,
|
||||
enrichEndpointItems,
|
||||
enrichItemsForDefaultRuleList,
|
||||
enrichItemsForSharedLists,
|
||||
entrichNewExceptionItems,
|
||||
} from './utils';
|
||||
|
||||
const getExceptionItems = (): ExceptionsBuilderReturnExceptionItem[] => [
|
||||
{ ...getCreateExceptionListItemSchemaMock(), os_types: [] },
|
||||
];
|
||||
|
||||
describe('add_exception_flyout#utils', () => {
|
||||
describe('entrichNewExceptionItems', () => {
|
||||
it('enriches exception items for rule default list', () => {
|
||||
const items = getExceptionItems();
|
||||
|
||||
expect(
|
||||
entrichNewExceptionItems({
|
||||
itemName: 'My item',
|
||||
commentToAdd: 'New comment',
|
||||
addToRules: true,
|
||||
addToSharedLists: false,
|
||||
sharedLists: [],
|
||||
selectedOs: [],
|
||||
listType: ExceptionListTypeEnum.RULE_DEFAULT,
|
||||
items,
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
...items[0],
|
||||
comments: [{ comment: 'New comment' }],
|
||||
list_id: undefined,
|
||||
name: 'My item',
|
||||
namespace_type: 'single',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('enriches exception items for shared lists', () => {
|
||||
const items = getExceptionItems();
|
||||
const sharedLists: ExceptionListSchema[] = [
|
||||
{
|
||||
...getExceptionListSchemaMock(),
|
||||
list_id: 'foo',
|
||||
namespace_type: 'single',
|
||||
},
|
||||
{
|
||||
...getExceptionListSchemaMock(),
|
||||
list_id: 'bar',
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
entrichNewExceptionItems({
|
||||
itemName: 'My item',
|
||||
commentToAdd: 'New comment',
|
||||
addToRules: false,
|
||||
addToSharedLists: true,
|
||||
sharedLists,
|
||||
selectedOs: [],
|
||||
listType: ExceptionListTypeEnum.DETECTION,
|
||||
items,
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
...items[0],
|
||||
comments: [{ comment: 'New comment' }],
|
||||
list_id: 'foo',
|
||||
name: 'My item',
|
||||
namespace_type: 'single',
|
||||
},
|
||||
{
|
||||
...items[0],
|
||||
comments: [{ comment: 'New comment' }],
|
||||
list_id: 'bar',
|
||||
name: 'My item',
|
||||
namespace_type: 'agnostic',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('enriches exception items for endpoint list', () => {
|
||||
const items: ExceptionsBuilderReturnExceptionItem[] = [
|
||||
{
|
||||
...getCreateExceptionListItemSchemaMock(),
|
||||
list_id: 'endpoint_list',
|
||||
namespace_type: 'agnostic',
|
||||
os_types: [],
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
entrichNewExceptionItems({
|
||||
itemName: 'My item',
|
||||
commentToAdd: 'New comment',
|
||||
addToRules: false,
|
||||
addToSharedLists: false,
|
||||
sharedLists: [],
|
||||
selectedOs: ['windows'],
|
||||
listType: ExceptionListTypeEnum.ENDPOINT,
|
||||
items,
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
...items[0],
|
||||
comments: [{ comment: 'New comment' }],
|
||||
list_id: 'endpoint_list',
|
||||
name: 'My item',
|
||||
namespace_type: 'agnostic',
|
||||
os_types: ['windows'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enrichItemWithComment', () => {
|
||||
it('returns items unchanged if no comment', () => {
|
||||
const items = getExceptionItems();
|
||||
|
||||
expect(enrichItemWithComment(' ')(items)).toEqual(items);
|
||||
});
|
||||
|
||||
it('returns items with new comment', () => {
|
||||
const items = getExceptionItems();
|
||||
|
||||
expect(enrichItemWithComment('My new comment')(items)).toEqual([
|
||||
{
|
||||
...items[0],
|
||||
comments: [
|
||||
{
|
||||
comment: 'My new comment',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enrichItemWithName', () => {
|
||||
it('returns items unchanged if no name', () => {
|
||||
const items = getExceptionItems();
|
||||
|
||||
expect(enrichItemWithName(' ')(items)).toEqual(items);
|
||||
});
|
||||
|
||||
it('returns items with name', () => {
|
||||
const items = getExceptionItems();
|
||||
|
||||
expect(enrichItemWithName('My item')(items)).toEqual([
|
||||
{
|
||||
...items[0],
|
||||
name: 'My item',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enrichEndpointItems', () => {
|
||||
it('returns items unchanged if "listType" is not "endpoint"', () => {
|
||||
const items = getExceptionItems();
|
||||
|
||||
expect(enrichEndpointItems(ExceptionListTypeEnum.DETECTION, [])(items)).toEqual(items);
|
||||
});
|
||||
|
||||
it('returns items with os types', () => {
|
||||
const items: ExceptionsBuilderReturnExceptionItem[] = [
|
||||
{
|
||||
...getCreateExceptionListItemSchemaMock(),
|
||||
namespace_type: 'agnostic',
|
||||
list_id: 'endpoint_list',
|
||||
},
|
||||
];
|
||||
|
||||
expect(enrichEndpointItems(ExceptionListTypeEnum.ENDPOINT, ['windows'])(items)).toEqual([
|
||||
{
|
||||
...items[0],
|
||||
os_types: ['windows'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enrichItemsForDefaultRuleList', () => {
|
||||
it('returns items unchanged if "addToRules" is "false"', () => {
|
||||
const items = getExceptionItems();
|
||||
|
||||
expect(enrichItemsForDefaultRuleList(ExceptionListTypeEnum.DETECTION, false)(items)).toEqual(
|
||||
items
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
* Wouldn't make sense for the second argument to be "true", when
|
||||
* listType is endpoint, but figured it'd be good
|
||||
* to test anyways.
|
||||
*/
|
||||
it('returns items unchanged if "listType" is "endpoint"', () => {
|
||||
const items = getExceptionItems();
|
||||
|
||||
expect(enrichItemsForDefaultRuleList(ExceptionListTypeEnum.ENDPOINT, true)(items)).toEqual(
|
||||
items
|
||||
);
|
||||
});
|
||||
|
||||
it('returns items with to add for each shared list', () => {
|
||||
const items = getExceptionItems();
|
||||
|
||||
expect(enrichItemsForDefaultRuleList(ExceptionListTypeEnum.DETECTION, true)(items)).toEqual([
|
||||
{
|
||||
...items[0],
|
||||
list_id: undefined,
|
||||
namespace_type: 'single',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enrichItemsForSharedLists', () => {
|
||||
it('returns items unchanged if "addToSharedLists" is "false"', () => {
|
||||
const items = getExceptionItems();
|
||||
|
||||
expect(enrichItemsForSharedLists(ExceptionListTypeEnum.DETECTION, false, [])(items)).toEqual(
|
||||
items
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
* Wouldn't make sense for the second argument to be "true", when
|
||||
* listType is endpoint, but figured it'd be good
|
||||
* to test anyways.
|
||||
*/
|
||||
it('returns items unchanged if "listType" is "endpoint"', () => {
|
||||
const items = getExceptionItems();
|
||||
|
||||
expect(enrichItemsForSharedLists(ExceptionListTypeEnum.ENDPOINT, true, [])(items)).toEqual(
|
||||
items
|
||||
);
|
||||
});
|
||||
|
||||
it('returns items with to add for each shared list', () => {
|
||||
const items = getExceptionItems();
|
||||
const sharedLists: ExceptionListSchema[] = [
|
||||
{
|
||||
...getExceptionListSchemaMock(),
|
||||
list_id: 'foo',
|
||||
namespace_type: 'single',
|
||||
},
|
||||
{
|
||||
...getExceptionListSchemaMock(),
|
||||
list_id: 'bar',
|
||||
},
|
||||
];
|
||||
expect(
|
||||
enrichItemsForSharedLists(ExceptionListTypeEnum.DETECTION, true, sharedLists)(items)
|
||||
).toEqual([
|
||||
{
|
||||
...items[0],
|
||||
list_id: 'foo',
|
||||
namespace_type: 'single',
|
||||
},
|
||||
{
|
||||
...items[0],
|
||||
list_id: 'bar',
|
||||
namespace_type: 'agnostic',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,269 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { pipe } from 'lodash/fp';
|
||||
|
||||
import type { ExceptionListSchema, OsType } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import type { ExceptionsBuilderReturnExceptionItem } from '@kbn/securitysolution-list-utils';
|
||||
|
||||
import {
|
||||
enrichExceptionItemsWithOS,
|
||||
enrichNewExceptionItemsWithComments,
|
||||
enrichNewExceptionItemsWithName,
|
||||
enrichRuleExceptions,
|
||||
enrichSharedExceptions,
|
||||
lowercaseHashValues,
|
||||
} from '../../utils/helpers';
|
||||
import { SecuritySolutionLinkAnchor } from '../../../../common/components/links';
|
||||
import { getRuleDetailsTabUrl } from '../../../../common/components/link_to/redirect_to_detection_engine';
|
||||
import { RuleDetailTabs } from '../../../../detections/pages/detection_engine/rules/details';
|
||||
import { SecurityPageName } from '../../../../../common/constants';
|
||||
import { PopoverItems } from '../../../../common/components/popover_items';
|
||||
import type {
|
||||
ExceptionListRuleReferencesInfoSchema,
|
||||
ExceptionListRuleReferencesSchema,
|
||||
} from '../../../../../common/detection_engine/schemas/response';
|
||||
import type { Rule } from '../../../../detections/containers/detection_engine/rules/types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
/**
|
||||
* Adds user defined name to all new exceptionItems
|
||||
* @param commentToAdd new comment to add to item
|
||||
*/
|
||||
export const enrichItemWithComment =
|
||||
(commentToAdd: string) =>
|
||||
(items: ExceptionsBuilderReturnExceptionItem[]): ExceptionsBuilderReturnExceptionItem[] => {
|
||||
return commentToAdd.trim() !== ''
|
||||
? enrichNewExceptionItemsWithComments(items, [{ comment: commentToAdd }])
|
||||
: items;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds user defined name to all new exceptionItems
|
||||
* @param itemName exception item name
|
||||
*/
|
||||
export const enrichItemWithName =
|
||||
(itemName: string) => (items: ExceptionsBuilderReturnExceptionItem[]) => {
|
||||
return itemName.trim() !== '' ? enrichNewExceptionItemsWithName(items, itemName) : items;
|
||||
};
|
||||
|
||||
/**
|
||||
* Modifies item entries to be in correct format and adds os selection to items
|
||||
* @param listType exception list type
|
||||
* @param selectedOs os selection
|
||||
*/
|
||||
export const enrichEndpointItems =
|
||||
(listType: ExceptionListTypeEnum, selectedOs: OsType[]) =>
|
||||
(items: ExceptionsBuilderReturnExceptionItem[]) => {
|
||||
if (listType === ExceptionListTypeEnum.ENDPOINT) {
|
||||
return lowercaseHashValues(enrichExceptionItemsWithOS(items, selectedOs));
|
||||
} else {
|
||||
return items;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Modifies exception items to prepare for creating as rule_default
|
||||
* list items
|
||||
* @param listType exception list type
|
||||
* @param addToRules boolean determining if user selected to add items to default rule list
|
||||
*/
|
||||
export const enrichItemsForDefaultRuleList =
|
||||
(listType: ExceptionListTypeEnum, addToRules: boolean) =>
|
||||
(items: ExceptionsBuilderReturnExceptionItem[]) => {
|
||||
if (addToRules && listType !== ExceptionListTypeEnum.ENDPOINT) {
|
||||
return enrichRuleExceptions(items);
|
||||
} else {
|
||||
return items;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepares items to be added to shared exception lists
|
||||
* @param listType exception list type
|
||||
* @param addToSharedLists boolean determining if user selected to add items to shared list
|
||||
* @param lists shared exception lists that were selected to add items to
|
||||
*/
|
||||
export const enrichItemsForSharedLists =
|
||||
(listType: ExceptionListTypeEnum, addToSharedLists: boolean, lists: ExceptionListSchema[]) =>
|
||||
(items: ExceptionsBuilderReturnExceptionItem[]) => {
|
||||
if (addToSharedLists && listType !== ExceptionListTypeEnum.ENDPOINT) {
|
||||
return enrichSharedExceptions(items, lists);
|
||||
} else {
|
||||
return items;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Series of utils to modify and prepare exception items for update or creation
|
||||
* @param itemName user defined exception item name
|
||||
* @param commentToAdd comment to be added to item
|
||||
* @param addToRules boolean determining if user selected to add items to default rule list
|
||||
* @param addToSharedLists boolean determining if user selected to add items to shared list
|
||||
* @param sharedLists shared exception lists that were selected to add items to
|
||||
* @param selectedOs os selection
|
||||
* @param listType exception list type
|
||||
* @param items exception items to be modified
|
||||
*/
|
||||
export const entrichNewExceptionItems = ({
|
||||
itemName,
|
||||
commentToAdd,
|
||||
addToRules,
|
||||
addToSharedLists,
|
||||
sharedLists,
|
||||
selectedOs,
|
||||
listType,
|
||||
items,
|
||||
}: {
|
||||
itemName: string;
|
||||
commentToAdd: string;
|
||||
selectedOs: OsType[];
|
||||
addToRules: boolean;
|
||||
addToSharedLists: boolean;
|
||||
sharedLists: ExceptionListSchema[];
|
||||
listType: ExceptionListTypeEnum;
|
||||
items: ExceptionsBuilderReturnExceptionItem[];
|
||||
}): ExceptionsBuilderReturnExceptionItem[] => {
|
||||
const enriched: ExceptionsBuilderReturnExceptionItem[] = pipe(
|
||||
enrichItemWithComment(commentToAdd),
|
||||
enrichItemWithName(itemName),
|
||||
enrichEndpointItems(listType, selectedOs),
|
||||
enrichItemsForDefaultRuleList(listType, addToRules),
|
||||
enrichItemsForSharedLists(listType, addToSharedLists, sharedLists)
|
||||
)(items);
|
||||
|
||||
return enriched;
|
||||
};
|
||||
|
||||
/**
|
||||
* Series of utils to modify and prepare exception items for update or creation
|
||||
* @param itemName user defined exception item name
|
||||
* @param commentToAdd comment to be added to item
|
||||
* @param addToRules boolean determining if user selected to add items to default rule list
|
||||
* @param addToSharedLists boolean determining if user selected to add items to shared list
|
||||
* @param sharedLists shared exception lists that were selected to add items to
|
||||
* @param selectedOs os selection
|
||||
* @param listType exception list type
|
||||
* @param items exception items to be modified
|
||||
*/
|
||||
export const entrichExceptionItemsForUpdate = ({
|
||||
itemName,
|
||||
commentToAdd,
|
||||
selectedOs,
|
||||
listType,
|
||||
items,
|
||||
}: {
|
||||
itemName: string;
|
||||
commentToAdd: string;
|
||||
selectedOs: OsType[];
|
||||
listType: ExceptionListTypeEnum;
|
||||
items: ExceptionsBuilderReturnExceptionItem[];
|
||||
}): ExceptionsBuilderReturnExceptionItem[] => {
|
||||
const enriched: ExceptionsBuilderReturnExceptionItem[] = pipe(
|
||||
enrichItemWithComment(commentToAdd),
|
||||
enrichItemWithName(itemName),
|
||||
enrichEndpointItems(listType, selectedOs)
|
||||
)(items);
|
||||
|
||||
return enriched;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared lists columns for EuiInMemoryTable
|
||||
*/
|
||||
export const getSharedListsTableColumns = () => [
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Name',
|
||||
sortable: true,
|
||||
'data-test-subj': 'exceptionListNameCell',
|
||||
},
|
||||
{
|
||||
field: 'referenced_rules',
|
||||
name: '# of rules linked to',
|
||||
sortable: false,
|
||||
'data-test-subj': 'exceptionListRulesLinkedToIdCell',
|
||||
render: (references: ExceptionListRuleReferencesInfoSchema[]) => {
|
||||
if (references.length === 0) return '0';
|
||||
|
||||
const renderItem = (reference: ExceptionListRuleReferencesInfoSchema, i: number) => (
|
||||
<SecuritySolutionLinkAnchor
|
||||
data-test-subj="referencedRuleLink"
|
||||
deepLinkId={SecurityPageName.rules}
|
||||
path={getRuleDetailsTabUrl(reference.id, RuleDetailTabs.alerts)}
|
||||
external
|
||||
>
|
||||
{reference.name}
|
||||
</SecuritySolutionLinkAnchor>
|
||||
);
|
||||
|
||||
return (
|
||||
<PopoverItems
|
||||
items={references}
|
||||
popoverButtonTitle={references.length.toString()}
|
||||
dataTestPrefix="ruleReferences"
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
// TODO: This will need to be updated once PR goes in with list details page
|
||||
{
|
||||
name: 'Actions',
|
||||
actions: [
|
||||
{
|
||||
'data-test-subj': 'exceptionListRulesActionCell',
|
||||
render: (list: ExceptionListRuleReferencesSchema) => {
|
||||
return (
|
||||
<SecuritySolutionLinkAnchor
|
||||
data-test-subj="exceptionListActionCell-link"
|
||||
deepLinkId={SecurityPageName.exceptions}
|
||||
path={''}
|
||||
external
|
||||
>
|
||||
{i18n.VIEW_LIST_DETAIL_ACTION}
|
||||
</SecuritySolutionLinkAnchor>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Rules columns for EuiInMemoryTable
|
||||
*/
|
||||
export const getRulesTableColumn = () => [
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Name',
|
||||
sortable: true,
|
||||
'data-test-subj': 'ruleNameCell',
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
actions: [
|
||||
{
|
||||
'data-test-subj': 'ruleAction-view',
|
||||
render: (rule: Rule) => {
|
||||
return (
|
||||
<SecuritySolutionLinkAnchor
|
||||
data-test-subj="ruleAction-viewDetails"
|
||||
deepLinkId={SecurityPageName.rules}
|
||||
path={getRuleDetailsTabUrl(rule.id, RuleDetailTabs.alerts)}
|
||||
external
|
||||
>
|
||||
{i18n.VIEW_RULE_DETAIL_ACTION}
|
||||
</SecuritySolutionLinkAnchor>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { EuiTextArea } from '@elastic/eui';
|
||||
|
||||
import { ExceptionItemComments } from '.';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { useCurrentUser } from '../../../../common/lib/kibana';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
describe('ExceptionItemComments', () => {
|
||||
beforeEach(() => {
|
||||
(useCurrentUser as jest.Mock).mockReturnValue({
|
||||
username: 'user',
|
||||
email: 'email',
|
||||
fullName: 'full name',
|
||||
roles: ['user-role'],
|
||||
enabled: true,
|
||||
authentication_realm: { name: 'native1', type: 'native' },
|
||||
lookup_realm: { name: 'native1', type: 'native' },
|
||||
authentication_provider: { type: 'basic', name: 'basic1' },
|
||||
authentication_type: 'realm',
|
||||
elastic_cloud_user: false,
|
||||
metadata: { _reserved: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('it uses user full_name if one exists', () => {
|
||||
const wrapper = shallow(
|
||||
<ExceptionItemComments
|
||||
newCommentValue={'This is a new comment'}
|
||||
newCommentOnChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionItemCommentAvatar"]').prop('name')).toEqual(
|
||||
'full name'
|
||||
);
|
||||
});
|
||||
|
||||
it('it uses user email if fullName is not available', () => {
|
||||
(useCurrentUser as jest.Mock).mockReturnValue({
|
||||
username: 'user',
|
||||
email: 'email',
|
||||
fullName: '',
|
||||
roles: ['user-role'],
|
||||
enabled: true,
|
||||
authentication_realm: { name: 'native1', type: 'native' },
|
||||
lookup_realm: { name: 'native1', type: 'native' },
|
||||
authentication_provider: { type: 'basic', name: 'basic1' },
|
||||
authentication_type: 'realm',
|
||||
elastic_cloud_user: false,
|
||||
metadata: { _reserved: false },
|
||||
});
|
||||
|
||||
const wrapper = shallow(
|
||||
<ExceptionItemComments
|
||||
newCommentValue={'This is a new comment'}
|
||||
newCommentOnChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionItemCommentAvatar"]').prop('name')).toEqual(
|
||||
'email'
|
||||
);
|
||||
});
|
||||
|
||||
it('it uses username if fullName and email are not available', () => {
|
||||
(useCurrentUser as jest.Mock).mockReturnValue({
|
||||
username: 'user',
|
||||
email: '',
|
||||
fullName: '',
|
||||
roles: ['user-role'],
|
||||
enabled: true,
|
||||
authentication_realm: { name: 'native1', type: 'native' },
|
||||
lookup_realm: { name: 'native1', type: 'native' },
|
||||
authentication_provider: { type: 'basic', name: 'basic1' },
|
||||
authentication_type: 'realm',
|
||||
elastic_cloud_user: false,
|
||||
metadata: { _reserved: false },
|
||||
});
|
||||
|
||||
const wrapper = shallow(
|
||||
<ExceptionItemComments
|
||||
newCommentValue={'This is a new comment'}
|
||||
newCommentOnChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionItemCommentAvatar"]').prop('name')).toEqual(
|
||||
'user'
|
||||
);
|
||||
});
|
||||
|
||||
it('it renders new comment', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionItemComments
|
||||
newCommentValue={'This is a new comment'}
|
||||
newCommentOnChange={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionItemCommentsAccordion"]').exists()).toBeFalsy();
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="newExceptionItemCommentTextArea"]').at(1).props().value
|
||||
).toEqual('This is a new comment');
|
||||
});
|
||||
|
||||
it('it calls newCommentOnChange on comment update change', () => {
|
||||
const mockOnCommentChange = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionItemComments
|
||||
newCommentValue="This is a new comment"
|
||||
newCommentOnChange={mockOnCommentChange}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
(
|
||||
wrapper.find(EuiTextArea).at(0).props() as unknown as {
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
).onChange({
|
||||
target: { value: 'Updating my new comment' },
|
||||
} as React.ChangeEvent<HTMLInputElement>);
|
||||
|
||||
expect(mockOnCommentChange).toHaveBeenCalledWith('Updating my new comment');
|
||||
});
|
||||
|
||||
it('it renders existing comments if any exist', () => {
|
||||
const mockOnCommentChange = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<TestProviders>
|
||||
<ExceptionItemComments
|
||||
exceptionItemComments={[
|
||||
{
|
||||
comment: 'This is an existing comment',
|
||||
created_at: '2020-04-20T15:25:31.830Z',
|
||||
created_by: 'elastic',
|
||||
id: 'uuid_here',
|
||||
},
|
||||
]}
|
||||
newCommentValue={''}
|
||||
newCommentOnChange={mockOnCommentChange}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="exceptionItemCommentsAccordion"]').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -18,13 +18,14 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import type { Comment } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import * as i18n from '../../utils/translations';
|
||||
import * as i18n from './translations';
|
||||
import { useCurrentUser } from '../../../../common/lib/kibana';
|
||||
import { getFormattedComments } from '../../utils/helpers';
|
||||
|
||||
interface ExceptionItemCommentsProps {
|
||||
exceptionItemComments?: Comment[];
|
||||
newCommentValue: string;
|
||||
accordionTitle?: JSX.Element;
|
||||
newCommentOnChange: (value: string) => void;
|
||||
}
|
||||
|
||||
|
@ -48,10 +49,27 @@ const CommentAccordion = styled(EuiAccordion)`
|
|||
export const ExceptionItemComments = memo(function ExceptionItemComments({
|
||||
exceptionItemComments,
|
||||
newCommentValue,
|
||||
accordionTitle,
|
||||
newCommentOnChange,
|
||||
}: ExceptionItemCommentsProps) {
|
||||
const [shouldShowComments, setShouldShowComments] = useState(false);
|
||||
const currentUser = useCurrentUser();
|
||||
const fullName = currentUser?.fullName;
|
||||
const userName = currentUser?.username;
|
||||
const userEmail = currentUser?.email;
|
||||
const avatarName = useMemo(() => {
|
||||
if (fullName && fullName.length > 0) {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
// Did email second because for cloud users, username is a uuid,
|
||||
// so favor using name or email prior to using the cloud generated id
|
||||
if (userEmail && userEmail.length > 0) {
|
||||
return userEmail;
|
||||
}
|
||||
|
||||
return userName && userName.length > 0 ? userName : i18n.UNKNOWN_AVATAR_NAME;
|
||||
}, [fullName, userEmail, userName]);
|
||||
|
||||
const handleOnChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
|
@ -64,7 +82,7 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({
|
|||
setShouldShowComments(isOpen);
|
||||
}, []);
|
||||
|
||||
const shouldShowAccordion: boolean = useMemo(() => {
|
||||
const exceptionItemsExist: boolean = useMemo(() => {
|
||||
return exceptionItemComments != null && exceptionItemComments.length > 0;
|
||||
}, [exceptionItemComments]);
|
||||
|
||||
|
@ -92,12 +110,12 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({
|
|||
|
||||
return (
|
||||
<div>
|
||||
{shouldShowAccordion && (
|
||||
{exceptionItemsExist && (
|
||||
<CommentAccordion
|
||||
id={'add-exception-comments-accordion'}
|
||||
buttonClassName={COMMENT_ACCORDION_BUTTON_CLASS_NAME}
|
||||
buttonContent={commentsAccordionTitle}
|
||||
data-test-subj="ExceptionItemCommentsAccordion"
|
||||
buttonContent={accordionTitle ?? commentsAccordionTitle}
|
||||
data-test-subj="exceptionItemCommentsAccordion"
|
||||
onToggle={(isOpen) => handleTriggerOnClick(isOpen)}
|
||||
>
|
||||
<EuiCommentList comments={formattedComments} />
|
||||
|
@ -105,10 +123,7 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({
|
|||
)}
|
||||
<EuiFlexGroup gutterSize={'none'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<MyAvatar
|
||||
name={currentUser != null ? currentUser.username.toUpperCase() ?? '' : ''}
|
||||
size="l"
|
||||
/>
|
||||
<MyAvatar name={avatarName} size="l" data-test-subj="exceptionItemCommentAvatar" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiTextArea
|
||||
|
@ -117,6 +132,7 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({
|
|||
value={newCommentValue}
|
||||
onChange={handleOnChange}
|
||||
fullWidth={true}
|
||||
data-test-subj="newExceptionItemCommentTextArea"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 UNKNOWN_AVATAR_NAME = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.itemComments.unknownAvatarName',
|
||||
{
|
||||
defaultMessage: 'Uknown',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_COMMENT_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.rule_exceptions.itemComments.addCommentPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Add a new comment...',
|
||||
}
|
||||
);
|
||||
|
||||
export const COMMENTS_SHOW = (comments: number) =>
|
||||
i18n.translate('xpack.securitySolution.rule_exceptions.itemComments.showCommentsLabel', {
|
||||
values: { comments },
|
||||
defaultMessage: 'Show ({comments}) {comments, plural, =1 {Comment} other {Comments}}',
|
||||
});
|
||||
|
||||
export const COMMENTS_HIDE = (comments: number) =>
|
||||
i18n.translate('xpack.securitySolution.rule_exceptions.itemComments.hideCommentsLabel', {
|
||||
values: { comments },
|
||||
defaultMessage: 'Hide ({comments}) {comments, plural, =1 {Comment} other {Comments}}',
|
||||
});
|
|
@ -5,51 +5,49 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import type { ListArray } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { RuleReferenceSchema } from '../../../../common/detection_engine/schemas/response';
|
||||
import type { ExceptionListRuleReferencesSchema } from '../../../../common/detection_engine/schemas/response';
|
||||
import { findRuleExceptionReferences } from '../../../detections/containers/detection_engine/rules/api';
|
||||
import { useToasts } from '../../../common/lib/kibana';
|
||||
import type { FindRulesReferencedByExceptionsListProp } from '../../../detections/containers/detection_engine/rules/types';
|
||||
import * as i18n from '../utils/translations';
|
||||
|
||||
export type ReturnUseFindExceptionListReferences = [boolean, boolean, RuleReferences | null];
|
||||
|
||||
export interface RuleReferences {
|
||||
[key: string]: RuleReferenceSchema[];
|
||||
[key: string]: ExceptionListRuleReferencesSchema;
|
||||
}
|
||||
|
||||
export type FetchReferencesFunc = (
|
||||
listsToFetch: FindRulesReferencedByExceptionsListProp[]
|
||||
) => Promise<void>;
|
||||
|
||||
export type ReturnUseFindExceptionListReferences = [
|
||||
boolean,
|
||||
boolean,
|
||||
RuleReferences | null,
|
||||
FetchReferencesFunc | null
|
||||
];
|
||||
|
||||
/**
|
||||
* Hook for finding what rules are referenced by a set of exception lists
|
||||
* @param ruleExceptionLists array of exception list info stored on a rule
|
||||
*/
|
||||
export const useFindExceptionListReferences = (
|
||||
ruleExceptionLists: ListArray
|
||||
): ReturnUseFindExceptionListReferences => {
|
||||
export const useFindExceptionListReferences = (): ReturnUseFindExceptionListReferences => {
|
||||
const toasts = useToasts();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errorExists, setErrorExists] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [didError, setError] = useState(false);
|
||||
const [references, setReferences] = useState<RuleReferences | null>(null);
|
||||
const listRefs = useMemo((): FindRulesReferencedByExceptionsListProp[] => {
|
||||
return ruleExceptionLists.map((list) => {
|
||||
return {
|
||||
id: list.id,
|
||||
listId: list.list_id,
|
||||
namespaceType: list.namespace_type,
|
||||
};
|
||||
});
|
||||
}, [ruleExceptionLists]);
|
||||
const findExceptionListAndReferencesRef = useRef<FetchReferencesFunc | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const findReferences = async () => {
|
||||
const findReferences = async (listsToFetch: FindRulesReferencedByExceptionsListProp[]) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const { references: referencesResults } = await findRuleExceptionReferences({
|
||||
lists: listRefs,
|
||||
lists: listsToFetch,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
|
@ -62,32 +60,26 @@ export const useFindExceptionListReferences = (
|
|||
}, {});
|
||||
|
||||
if (isSubscribed) {
|
||||
setErrorExists(false);
|
||||
setIsLoading(false);
|
||||
setError(false);
|
||||
setReferences(results);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
setErrorExists(true);
|
||||
setError(true);
|
||||
setIsLoading(false);
|
||||
toasts.addError(error, { title: i18n.ERROR_FETCHING_REFERENCES_TITLE });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (listRefs.length === 0 && isSubscribed) {
|
||||
setErrorExists(false);
|
||||
setIsLoading(false);
|
||||
setReferences(null);
|
||||
} else {
|
||||
findReferences();
|
||||
}
|
||||
findExceptionListAndReferencesRef.current = findReferences;
|
||||
|
||||
return (): void => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [ruleExceptionLists, listRefs, toasts]);
|
||||
}, [toasts]);
|
||||
|
||||
return [isLoading, errorExists, references];
|
||||
return [isLoading, didError, references, findExceptionListAndReferencesRef.current];
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ import type {
|
|||
ExceptionListItemSchema,
|
||||
CreateExceptionListItemSchema,
|
||||
UpdateExceptionListItemSchema,
|
||||
ExceptionListSchema,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import {
|
||||
comment,
|
||||
|
@ -30,7 +31,10 @@ import {
|
|||
ListOperatorTypeEnum as OperatorTypeEnum,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import type { ExceptionsBuilderExceptionItem } from '@kbn/securitysolution-list-utils';
|
||||
import type {
|
||||
ExceptionsBuilderExceptionItem,
|
||||
ExceptionsBuilderReturnExceptionItem,
|
||||
} from '@kbn/securitysolution-list-utils';
|
||||
import {
|
||||
getOperatorType,
|
||||
getNewExceptionItem,
|
||||
|
@ -169,10 +173,10 @@ export const prepareExceptionItemsForBulkClose = (
|
|||
* @param comments new Comment
|
||||
*/
|
||||
export const enrichNewExceptionItemsWithComments = (
|
||||
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>,
|
||||
exceptionItems: ExceptionsBuilderReturnExceptionItem[],
|
||||
comments: Array<Comment | CreateComment>
|
||||
): Array<ExceptionListItemSchema | CreateExceptionListItemSchema> => {
|
||||
return exceptionItems.map((item: ExceptionListItemSchema | CreateExceptionListItemSchema) => {
|
||||
): ExceptionsBuilderReturnExceptionItem[] => {
|
||||
return exceptionItems.map((item: ExceptionsBuilderReturnExceptionItem) => {
|
||||
return {
|
||||
...item,
|
||||
comments,
|
||||
|
@ -197,9 +201,9 @@ export const buildGetAlertByIdQuery = (id: string | undefined) => ({
|
|||
* and new comments
|
||||
*/
|
||||
export const enrichExistingExceptionItemWithComments = (
|
||||
exceptionItem: ExceptionListItemSchema | CreateExceptionListItemSchema,
|
||||
exceptionItem: ExceptionsBuilderReturnExceptionItem,
|
||||
comments: Array<Comment | CreateComment>
|
||||
): ExceptionListItemSchema | CreateExceptionListItemSchema => {
|
||||
): ExceptionsBuilderReturnExceptionItem => {
|
||||
const formattedComments = comments.map((item) => {
|
||||
if (comment.is(item)) {
|
||||
const { id, comment: existingComment } = item;
|
||||
|
@ -226,10 +230,10 @@ export const enrichExistingExceptionItemWithComments = (
|
|||
* @param osTypes array of os values
|
||||
*/
|
||||
export const enrichExceptionItemsWithOS = (
|
||||
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>,
|
||||
exceptionItems: ExceptionsBuilderReturnExceptionItem[],
|
||||
osTypes: OsTypeArray
|
||||
): Array<ExceptionListItemSchema | CreateExceptionListItemSchema> => {
|
||||
return exceptionItems.map((item: ExceptionListItemSchema | CreateExceptionListItemSchema) => {
|
||||
): ExceptionsBuilderReturnExceptionItem[] => {
|
||||
return exceptionItems.map((item: ExceptionsBuilderReturnExceptionItem) => {
|
||||
return {
|
||||
...item,
|
||||
os_types: osTypes,
|
||||
|
@ -255,8 +259,8 @@ export const retrieveAlertOsTypes = (alertData?: AlertData): OsTypeArray => {
|
|||
* Returns given exceptionItems with all hash-related entries lowercased
|
||||
*/
|
||||
export const lowercaseHashValues = (
|
||||
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>
|
||||
): Array<ExceptionListItemSchema | CreateExceptionListItemSchema> => {
|
||||
exceptionItems: ExceptionsBuilderReturnExceptionItem[]
|
||||
): ExceptionsBuilderReturnExceptionItem[] => {
|
||||
return exceptionItems.map((item) => {
|
||||
const newEntries = item.entries.map((itemEntry) => {
|
||||
if (itemEntry.field.includes('.hash')) {
|
||||
|
@ -281,9 +285,7 @@ export const lowercaseHashValues = (
|
|||
});
|
||||
};
|
||||
|
||||
export const entryHasListType = (
|
||||
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>
|
||||
) => {
|
||||
export const entryHasListType = (exceptionItems: ExceptionsBuilderReturnExceptionItem[]) => {
|
||||
for (const { entries } of exceptionItems) {
|
||||
for (const exceptionEntry of entries ?? []) {
|
||||
if (getOperatorType(exceptionEntry) === OperatorTypeEnum.LIST) {
|
||||
|
@ -755,7 +757,7 @@ export const getPrepopulatedBehaviorException = ({
|
|||
* Determines whether or not any entries within the given exceptionItems contain values not in the specified ECS mapping
|
||||
*/
|
||||
export const entryHasNonEcsType = (
|
||||
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>,
|
||||
exceptionItems: ExceptionsBuilderReturnExceptionItem[],
|
||||
indexPatterns: DataViewBase
|
||||
): boolean => {
|
||||
const doesFieldNameExist = (exceptionEntry: Entry): boolean => {
|
||||
|
@ -842,3 +844,57 @@ export const defaultEndpointExceptionItems = (
|
|||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds user defined name to all new exceptionItems
|
||||
* @param exceptionItems new or existing ExceptionItem[]
|
||||
* @param name new exception item name
|
||||
*/
|
||||
export const enrichNewExceptionItemsWithName = (
|
||||
exceptionItems: ExceptionsBuilderReturnExceptionItem[],
|
||||
name: string
|
||||
): ExceptionsBuilderReturnExceptionItem[] => {
|
||||
return exceptionItems.map((item: ExceptionsBuilderReturnExceptionItem) => {
|
||||
return {
|
||||
...item,
|
||||
name,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Modifies exception items to prepare for creating as rule_default
|
||||
* list items
|
||||
* @param exceptionItems new or existing ExceptionItem[]
|
||||
*/
|
||||
export const enrichRuleExceptions = (
|
||||
exceptionItems: ExceptionsBuilderReturnExceptionItem[]
|
||||
): ExceptionsBuilderReturnExceptionItem[] => {
|
||||
return exceptionItems.map((item: ExceptionsBuilderReturnExceptionItem) => {
|
||||
return {
|
||||
...item,
|
||||
list_id: undefined,
|
||||
namespace_type: 'single',
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepares items to be added to shared exception lists
|
||||
* @param exceptionItems new or existing ExceptionItem[]
|
||||
* @param lists shared exception lists that were selected to add items to
|
||||
*/
|
||||
export const enrichSharedExceptions = (
|
||||
exceptionItems: ExceptionsBuilderReturnExceptionItem[],
|
||||
lists: ExceptionListSchema[]
|
||||
): ExceptionsBuilderReturnExceptionItem[] => {
|
||||
return lists.flatMap((list) => {
|
||||
return exceptionItems.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
list_id: list.list_id,
|
||||
namespace_type: list.namespace_type,
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -386,16 +386,23 @@ export const fetchInstalledIntegrations = async ({
|
|||
export const findRuleExceptionReferences = async ({
|
||||
lists,
|
||||
signal,
|
||||
}: FindRulesReferencedByExceptionsProps): Promise<RulesReferencedByExceptionListsSchema> =>
|
||||
KibanaServices.get().http.fetch<RulesReferencedByExceptionListsSchema>(
|
||||
DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL,
|
||||
{
|
||||
method: 'GET',
|
||||
query: {
|
||||
}: FindRulesReferencedByExceptionsProps): Promise<RulesReferencedByExceptionListsSchema> => {
|
||||
const idsUndefined = lists.some(({ id }) => id === undefined);
|
||||
const query = idsUndefined
|
||||
? {
|
||||
namespace_types: lists.map(({ namespaceType }) => namespaceType).join(','),
|
||||
}
|
||||
: {
|
||||
ids: lists.map(({ id }) => id).join(','),
|
||||
list_ids: lists.map(({ listId }) => listId).join(','),
|
||||
namespace_types: lists.map(({ namespaceType }) => namespaceType).join(','),
|
||||
},
|
||||
};
|
||||
return KibanaServices.get().http.fetch<RulesReferencedByExceptionListsSchema>(
|
||||
DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL,
|
||||
{
|
||||
method: 'GET',
|
||||
query,
|
||||
signal,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -344,8 +344,8 @@ export interface PrePackagedRulesStatusResponse {
|
|||
}
|
||||
|
||||
export interface FindRulesReferencedByExceptionsListProp {
|
||||
id: string;
|
||||
listId: string;
|
||||
id?: string;
|
||||
listId?: string;
|
||||
namespaceType: NamespaceType;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,10 +14,18 @@ import {
|
|||
import { requestContextMock, serverMock, requestMock } from '../__mocks__';
|
||||
import { findRuleExceptionReferencesRoute } from './find_rule_exceptions_route';
|
||||
import { getQueryRuleParams } from '../../schemas/rule_schemas.mock';
|
||||
import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock';
|
||||
|
||||
describe('findRuleExceptionReferencesRoute', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
const mockList = {
|
||||
...getExceptionListSchemaMock(),
|
||||
type: 'detection',
|
||||
id: '4656dc92-5832-11ea-8e2d-0242ac130003',
|
||||
list_id: 'my_default_list',
|
||||
namespace_type: 'single',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
server = serverMock.create();
|
||||
|
@ -42,6 +50,10 @@ describe('findRuleExceptionReferencesRoute', () => {
|
|||
],
|
||||
});
|
||||
|
||||
(clients.lists.exceptionListClient.findExceptionList as jest.Mock).mockResolvedValue({
|
||||
data: [mockList],
|
||||
});
|
||||
|
||||
findRuleExceptionReferencesRoute(server.router);
|
||||
});
|
||||
|
||||
|
@ -62,21 +74,24 @@ describe('findRuleExceptionReferencesRoute', () => {
|
|||
expect(response.body).toEqual({
|
||||
references: [
|
||||
{
|
||||
my_default_list: [
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: '4656dc92-5832-11ea-8e2d-0242ac130003',
|
||||
list_id: 'my_default_list',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
|
||||
name: 'Detect Root/Admin Users',
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
my_default_list: {
|
||||
...mockList,
|
||||
referenced_rules: [
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: '4656dc92-5832-11ea-8e2d-0242ac130003',
|
||||
list_id: 'my_default_list',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
|
||||
name: 'Detect Root/Admin Users',
|
||||
rule_id: 'rule-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -101,7 +116,10 @@ describe('findRuleExceptionReferencesRoute', () => {
|
|||
expect(response.body).toEqual({
|
||||
references: [
|
||||
{
|
||||
my_default_list: [],
|
||||
my_default_list: {
|
||||
...mockList,
|
||||
referenced_rules: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { getSavedObjectType } from '@kbn/securitysolution-list-utils';
|
||||
import { validate } from '@kbn/securitysolution-io-ts-utils';
|
||||
import type { FindResult } from '@kbn/alerting-plugin/server';
|
||||
|
||||
import type { SecuritySolutionPluginRouter } from '../../../../types';
|
||||
import { DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL } from '../../../../../common/constants';
|
||||
|
@ -43,39 +42,67 @@ export const findRuleExceptionReferencesRoute = (router: SecuritySolutionPluginR
|
|||
|
||||
const ctx = await context.resolve(['core', 'securitySolution', 'alerting']);
|
||||
const rulesClient = ctx.alerting.getRulesClient();
|
||||
const listsClient = ctx.securitySolution.getExceptionListClient();
|
||||
|
||||
if (ids.length !== namespaceTypes.length || ids.length !== listIds.length) {
|
||||
if (
|
||||
ids != null &&
|
||||
listIds != null &&
|
||||
(ids.length !== namespaceTypes.length || ids.length !== listIds.length)
|
||||
) {
|
||||
return siemResponse.error({
|
||||
body: `"ids", "list_ids" and "namespace_types" need to have the same comma separated number of values. Expected "ids" length: ${ids.length} to equal "namespace_types" length: ${namespaceTypes.length} and "list_ids" length: ${listIds.length}.`,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const foundRules: Array<FindResult<RuleParams>> = await Promise.all(
|
||||
ids.map(async (id, index) => {
|
||||
return rulesClient.find({
|
||||
const fetchExact = ids != null && listIds != null;
|
||||
|
||||
const foundExceptionLists = await listsClient?.findExceptionList({
|
||||
filter: fetchExact
|
||||
? `(${listIds
|
||||
.map((listId) => `exception-list.attributes.list_id:${listId}`)
|
||||
.join(' OR ')})`
|
||||
: undefined,
|
||||
namespaceType: ['agnostic', 'single'],
|
||||
page: 1,
|
||||
perPage: 10000,
|
||||
sortField: undefined,
|
||||
sortOrder: undefined,
|
||||
});
|
||||
|
||||
if (foundExceptionLists == null) {
|
||||
return response.ok({ body: { references: [] } });
|
||||
}
|
||||
|
||||
const references: RuleReferencesSchema[] = await Promise.all(
|
||||
foundExceptionLists.data.map(async (list, index) => {
|
||||
const foundRules = await rulesClient.find<RuleParams>({
|
||||
options: {
|
||||
perPage: 10000,
|
||||
filter: enrichFilterWithRuleTypeMapping(null),
|
||||
hasReference: {
|
||||
id,
|
||||
id: list.id,
|
||||
type: getSavedObjectType({ namespaceType: namespaceTypes[index] }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ruleData = foundRules.data.map(({ name, id, params }) => ({
|
||||
name,
|
||||
id,
|
||||
rule_id: params.ruleId,
|
||||
exception_lists: params.exceptionsList,
|
||||
}));
|
||||
|
||||
return {
|
||||
[list.list_id]: {
|
||||
...list,
|
||||
referenced_rules: ruleData,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const references = foundRules.map<RuleReferencesSchema>(({ data }, index) => {
|
||||
const wantedData = data.map(({ name, id, params }) => ({
|
||||
name,
|
||||
id,
|
||||
rule_id: params.ruleId,
|
||||
exception_lists: params.exceptionsList,
|
||||
}));
|
||||
return { [listIds[index]]: wantedData };
|
||||
});
|
||||
|
||||
const [validated, errors] = validate({ references }, rulesReferencedByExceptionListsSchema);
|
||||
|
||||
if (errors != null) {
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL } from '@kbn/security-solution-plugin/common/constants';
|
||||
|
@ -13,6 +15,7 @@ import {
|
|||
ExceptionListTypeEnum,
|
||||
} from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { getCreateExceptionListMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock';
|
||||
import { RuleReferencesSchema } from '@kbn/security-solution-plugin/common/detection_engine/schemas/response';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
createRule,
|
||||
|
@ -66,10 +69,46 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
expect(references).to.eql({ references: [{ i_exist: [] }] });
|
||||
const {
|
||||
_version,
|
||||
id,
|
||||
created_at,
|
||||
created_by,
|
||||
tie_breaker_id,
|
||||
updated_at,
|
||||
updated_by,
|
||||
...referencesWithoutServerValues
|
||||
} = references.references[0].i_exist;
|
||||
|
||||
expect({
|
||||
references: [
|
||||
{
|
||||
i_exist: {
|
||||
...referencesWithoutServerValues,
|
||||
},
|
||||
},
|
||||
],
|
||||
}).to.eql({
|
||||
references: [
|
||||
{
|
||||
i_exist: {
|
||||
description: 'some description',
|
||||
immutable: false,
|
||||
list_id: 'i_exist',
|
||||
name: 'some name',
|
||||
namespace_type: 'single',
|
||||
os_types: [],
|
||||
tags: [],
|
||||
type: 'detection',
|
||||
version: 1,
|
||||
referenced_rules: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty array per list_id if list does not exist', async () => {
|
||||
it('returns empty array per list_id if list does not exist', async () => {
|
||||
// create rule
|
||||
await createRule(supertest, log, getSimpleRule('rule-1'));
|
||||
|
||||
|
@ -83,7 +122,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
expect(references).to.eql({ references: [{ i_dont_exist: [] }] });
|
||||
expect(references).to.eql({ references: [] });
|
||||
});
|
||||
|
||||
it('returns found references', async () => {
|
||||
|
@ -101,7 +140,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
// create rule
|
||||
const rule = await createRule(supertest, log, {
|
||||
await createRule(supertest, log, {
|
||||
...getSimpleRule('rule-2'),
|
||||
exceptions_list: [
|
||||
{
|
||||
|
@ -129,56 +168,54 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
expect(references).to.eql({
|
||||
references: [
|
||||
const refs = references.references.flatMap((ref: RuleReferencesSchema) => Object.keys(ref));
|
||||
|
||||
expect(refs.sort()).to.eql(['i_exist', 'i_exist_2'].sort());
|
||||
});
|
||||
|
||||
it('returns found references for all existing exception lists if no list id/list_id passed in', async () => {
|
||||
// create exception list
|
||||
const newExceptionList: CreateExceptionListSchema = {
|
||||
...getCreateExceptionListMinimalSchemaMock(),
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: ExceptionListTypeEnum.DETECTION,
|
||||
};
|
||||
const exceptionList = await createExceptionList(supertest, log, newExceptionList);
|
||||
const exceptionList2 = await createExceptionList(supertest, log, {
|
||||
...newExceptionList,
|
||||
list_id: 'i_exist_2',
|
||||
});
|
||||
|
||||
// create rule
|
||||
await createRule(supertest, log, {
|
||||
...getSimpleRule('rule-2'),
|
||||
exceptions_list: [
|
||||
{
|
||||
i_exist: [
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: references.references[0].i_exist[0].exception_lists[0].id,
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: references.references[0].i_exist[0].exception_lists[1].id,
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: rule.id,
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
},
|
||||
],
|
||||
id: `${exceptionList.id}`,
|
||||
list_id: `${exceptionList.list_id}`,
|
||||
namespace_type: `${exceptionList.namespace_type}`,
|
||||
type: `${exceptionList.type}`,
|
||||
},
|
||||
{
|
||||
i_exist_2: [
|
||||
{
|
||||
exception_lists: [
|
||||
{
|
||||
id: references.references[1].i_exist_2[0].exception_lists[0].id,
|
||||
list_id: 'i_exist',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
{
|
||||
id: references.references[1].i_exist_2[0].exception_lists[1].id,
|
||||
list_id: 'i_exist_2',
|
||||
namespace_type: 'single',
|
||||
type: 'detection',
|
||||
},
|
||||
],
|
||||
id: rule.id,
|
||||
name: 'Simple Rule Query',
|
||||
rule_id: 'rule-2',
|
||||
},
|
||||
],
|
||||
id: `${exceptionList2.id}`,
|
||||
list_id: `${exceptionList2.list_id}`,
|
||||
namespace_type: `${exceptionList2.namespace_type}`,
|
||||
type: `${exceptionList2.type}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { body: references } = await supertest
|
||||
.get(DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.query({
|
||||
namespace_types: `${exceptionList.namespace_type},${exceptionList2.namespace_type}`,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const refs = references.references.flatMap((ref: RuleReferencesSchema) => Object.keys(ref));
|
||||
expect(refs.sort()).to.eql(['i_exist', 'i_exist_2', 'endpoint_list'].sort());
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue