mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Exceptions] - Exceptions modal pt 2 (#70886)
* makes comment updates * adds tests * adds back non ecs data to timeline * comments * fixes jest tests * fixes typo
This commit is contained in:
parent
87c8de8c7d
commit
c1b26651bd
11 changed files with 430 additions and 80 deletions
|
@ -43,6 +43,7 @@ import {
|
|||
defaultEndpointExceptionItems,
|
||||
entryHasListType,
|
||||
entryHasNonEcsType,
|
||||
getMappedNonEcsValue,
|
||||
} from '../helpers';
|
||||
import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
|
||||
|
||||
|
@ -65,7 +66,7 @@ interface AddExceptionModalProps {
|
|||
nonEcsData: TimelineNonEcsData[];
|
||||
};
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
onConfirm: (didCloseAlert: boolean) => void;
|
||||
}
|
||||
|
||||
const Modal = styled(EuiModal)`
|
||||
|
@ -130,8 +131,8 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
);
|
||||
const onSuccess = useCallback(() => {
|
||||
displaySuccessToast(i18n.ADD_EXCEPTION_SUCCESS, dispatchToaster);
|
||||
onConfirm();
|
||||
}, [dispatchToaster, onConfirm]);
|
||||
onConfirm(shouldCloseAlert);
|
||||
}, [dispatchToaster, onConfirm, shouldCloseAlert]);
|
||||
|
||||
const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException(
|
||||
{
|
||||
|
@ -193,6 +194,12 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
indexPatterns,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldDisableBulkClose === true) {
|
||||
setShouldBulkCloseAlert(false);
|
||||
}
|
||||
}, [shouldDisableBulkClose]);
|
||||
|
||||
const onCommentChange = useCallback(
|
||||
(value: string) => {
|
||||
setComment(value);
|
||||
|
@ -214,6 +221,21 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
[setShouldBulkCloseAlert]
|
||||
);
|
||||
|
||||
const retrieveAlertOsTypes = useCallback(() => {
|
||||
const osDefaults = ['windows', 'macos', 'linux'];
|
||||
if (alertData) {
|
||||
const osTypes = getMappedNonEcsValue({
|
||||
data: alertData.nonEcsData,
|
||||
fieldName: 'host.os.family',
|
||||
});
|
||||
if (osTypes.length === 0) {
|
||||
return osDefaults;
|
||||
}
|
||||
return osTypes;
|
||||
}
|
||||
return osDefaults;
|
||||
}, [alertData]);
|
||||
|
||||
const enrichExceptionItems = useCallback(() => {
|
||||
let enriched: Array<ExceptionListItemSchema | CreateExceptionListItemSchema> = [];
|
||||
enriched =
|
||||
|
@ -221,11 +243,11 @@ export const AddExceptionModal = memo(function AddExceptionModal({
|
|||
? enrichExceptionItemsWithComments(exceptionItemsToAdd, [{ comment }])
|
||||
: exceptionItemsToAdd;
|
||||
if (exceptionListType === 'endpoint') {
|
||||
const osTypes = alertData ? ['windows'] : ['windows', 'macos', 'linux'];
|
||||
const osTypes = retrieveAlertOsTypes();
|
||||
enriched = enrichExceptionItemsWithOS(enriched, osTypes);
|
||||
}
|
||||
return enriched;
|
||||
}, [comment, exceptionItemsToAdd, exceptionListType, alertData]);
|
||||
}, [comment, exceptionItemsToAdd, exceptionListType, retrieveAlertOsTypes]);
|
||||
|
||||
const onAddExceptionConfirm = useCallback(() => {
|
||||
if (addOrUpdateExceptionItems !== null) {
|
||||
|
|
|
@ -37,7 +37,7 @@ import { AddExceptionComments } from '../add_exception_comments';
|
|||
import {
|
||||
enrichExceptionItemsWithComments,
|
||||
enrichExceptionItemsWithOS,
|
||||
getOsTagValues,
|
||||
getOperatingSystems,
|
||||
entryHasListType,
|
||||
entryHasNonEcsType,
|
||||
} from '../helpers';
|
||||
|
@ -135,6 +135,12 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
indexPatterns,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldDisableBulkClose === true) {
|
||||
setShouldBulkCloseAlert(false);
|
||||
}
|
||||
}, [shouldDisableBulkClose]);
|
||||
|
||||
const handleBuilderOnChange = useCallback(
|
||||
({
|
||||
exceptionItems,
|
||||
|
@ -167,7 +173,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
...(comment !== '' ? [{ comment }] : []),
|
||||
]);
|
||||
if (exceptionListType === 'endpoint') {
|
||||
const osTypes = exceptionItem._tags ? getOsTagValues(exceptionItem._tags) : ['windows'];
|
||||
const osTypes = exceptionItem._tags ? getOperatingSystems(exceptionItem._tags) : [];
|
||||
enriched = enrichExceptionItemsWithOS(enriched, osTypes);
|
||||
}
|
||||
return enriched;
|
||||
|
@ -199,6 +205,8 @@ export const EditExceptionModal = memo(function EditExceptionModal({
|
|||
{!isSignalIndexLoading && (
|
||||
<>
|
||||
<ModalBodySection className="builder-section">
|
||||
<EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText>
|
||||
<EuiSpacer />
|
||||
<ExceptionBuilder
|
||||
exceptionListItems={[exceptionItem]}
|
||||
listType={exceptionListType}
|
||||
|
|
|
@ -47,7 +47,7 @@ export const ENDPOINT_QUARANTINE_TEXT = i18n.translate(
|
|||
);
|
||||
|
||||
export const EXCEPTION_BUILDER_INFO = i18n.translate(
|
||||
'xpack.securitySolution.exceptions.addException.infoLabel',
|
||||
'xpack.securitySolution.exceptions.editException.infoLabel',
|
||||
{
|
||||
defaultMessage: "Alerts are generated when the rule's conditions are met, except when:",
|
||||
}
|
||||
|
|
|
@ -18,6 +18,13 @@ import {
|
|||
getFormattedComments,
|
||||
filterExceptionItems,
|
||||
getNewExceptionItem,
|
||||
formatOperatingSystems,
|
||||
getEntryValue,
|
||||
formatExceptionItemForUpdate,
|
||||
enrichExceptionItemsWithComments,
|
||||
enrichExceptionItemsWithOS,
|
||||
entryHasListType,
|
||||
entryHasNonEcsType,
|
||||
} from './helpers';
|
||||
import { FormattedEntry, DescriptionListItem, EmptyEntry } from './types';
|
||||
import {
|
||||
|
@ -40,6 +47,9 @@ import {
|
|||
getEntriesArrayMock,
|
||||
} from '../../../../../lists/common/schemas/types/entries.mock';
|
||||
import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comments.mock';
|
||||
import { ENTRIES } from '../../../../../lists/common/constants.mock';
|
||||
import { ExceptionListItemSchema, EntriesArray } from '../../../../../lists/common/schemas';
|
||||
import { IIndexPattern } from 'src/plugins/data/common';
|
||||
|
||||
describe('Exception helpers', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -248,6 +258,36 @@ describe('Exception helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#getEntryValue', () => {
|
||||
it('returns "match" entry value', () => {
|
||||
const payload = getEntryMatchMock();
|
||||
const result = getEntryValue(payload);
|
||||
const expected = 'some host name';
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('returns "match any" entry values', () => {
|
||||
const payload = getEntryMatchAnyMock();
|
||||
const result = getEntryValue(payload);
|
||||
const expected = ['some host name'];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('returns "exists" entry value', () => {
|
||||
const payload = getEntryExistsMock();
|
||||
const result = getEntryValue(payload);
|
||||
const expected = undefined;
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('returns "list" entry value', () => {
|
||||
const payload = getEntryListMock();
|
||||
const result = getEntryValue(payload);
|
||||
const expected = 'some-list-id';
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#formatEntry', () => {
|
||||
test('it formats an entry', () => {
|
||||
const payload = getEntryMatchMock();
|
||||
|
@ -280,25 +320,55 @@ describe('Exception helpers', () => {
|
|||
test('it returns null if no operating system tag specified', () => {
|
||||
const result = getOperatingSystems(['some tag', 'some other tag']);
|
||||
|
||||
expect(result).toEqual('');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('it returns null if operating system tag malformed', () => {
|
||||
const result = getOperatingSystems(['some tag', 'jibberos:mac,windows', 'some other tag']);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('it returns operating systems if space included in os tag', () => {
|
||||
const result = getOperatingSystems(['some tag', 'os: macos', 'some other tag']);
|
||||
expect(result).toEqual(['macos']);
|
||||
});
|
||||
|
||||
test('it returns operating systems if multiple os tags specified', () => {
|
||||
const result = getOperatingSystems(['some tag', 'os: macos', 'some other tag', 'os:windows']);
|
||||
expect(result).toEqual(['macos', 'windows']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#formatOperatingSystems', () => {
|
||||
test('it returns null if no operating system tag specified', () => {
|
||||
const result = formatOperatingSystems(getOperatingSystems(['some tag', 'some other tag']));
|
||||
|
||||
expect(result).toEqual('');
|
||||
});
|
||||
|
||||
test('it returns null if operating system tag malformed', () => {
|
||||
const result = formatOperatingSystems(
|
||||
getOperatingSystems(['some tag', 'jibberos:mac,windows', 'some other tag'])
|
||||
);
|
||||
|
||||
expect(result).toEqual('');
|
||||
});
|
||||
|
||||
test('it returns formatted operating systems if space included in os tag', () => {
|
||||
const result = getOperatingSystems(['some tag', 'os: mac', 'some other tag']);
|
||||
const result = formatOperatingSystems(
|
||||
getOperatingSystems(['some tag', 'os: macos', 'some other tag'])
|
||||
);
|
||||
|
||||
expect(result).toEqual('Mac');
|
||||
expect(result).toEqual('macOS');
|
||||
});
|
||||
|
||||
test('it returns formatted operating systems if multiple os tags specified', () => {
|
||||
const result = getOperatingSystems(['some tag', 'os: mac', 'some other tag', 'os:windows']);
|
||||
const result = formatOperatingSystems(
|
||||
getOperatingSystems(['some tag', 'os: macos', 'some other tag', 'os:windows'])
|
||||
);
|
||||
|
||||
expect(result).toEqual('Mac, Windows');
|
||||
expect(result).toEqual('macOS, Windows');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -441,4 +511,176 @@ describe('Exception helpers', () => {
|
|||
expect(exceptions).toEqual([{ ...rest, meta: undefined }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#formatExceptionItemForUpdate', () => {
|
||||
test('it should return correct update fields', () => {
|
||||
const payload = getExceptionListItemSchemaMock();
|
||||
const result = formatExceptionItemForUpdate(payload);
|
||||
const expected = {
|
||||
_tags: ['endpoint', 'process', 'malware', 'os:linux'],
|
||||
comments: [],
|
||||
description: 'This is a sample endpoint type exception',
|
||||
entries: ENTRIES,
|
||||
id: '1',
|
||||
item_id: 'endpoint_list_item',
|
||||
meta: {},
|
||||
name: 'Sample Endpoint Exception List',
|
||||
namespace_type: 'single',
|
||||
tags: ['user added string for a tag', 'malware'],
|
||||
type: 'simple',
|
||||
};
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#enrichExceptionItemsWithComments', () => {
|
||||
test('it should add comments to an exception item', () => {
|
||||
const payload = [getExceptionListItemSchemaMock()];
|
||||
const comments = getCommentsArrayMock();
|
||||
const result = enrichExceptionItemsWithComments(payload, comments);
|
||||
const expected = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
comments: getCommentsArrayMock(),
|
||||
},
|
||||
];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it should add comments to multiple exception items', () => {
|
||||
const payload = [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()];
|
||||
const comments = getCommentsArrayMock();
|
||||
const result = enrichExceptionItemsWithComments(payload, comments);
|
||||
const expected = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
comments: getCommentsArrayMock(),
|
||||
},
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
comments: getCommentsArrayMock(),
|
||||
},
|
||||
];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#enrichExceptionItemsWithOS', () => {
|
||||
test('it should add an os tag to an exception item', () => {
|
||||
const payload = [getExceptionListItemSchemaMock()];
|
||||
const osTypes = ['windows'];
|
||||
const result = enrichExceptionItemsWithOS(payload, osTypes);
|
||||
const expected = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
_tags: [...getExceptionListItemSchemaMock()._tags, 'os:windows'],
|
||||
},
|
||||
];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it should add multiple os tags to all exception items', () => {
|
||||
const payload = [getExceptionListItemSchemaMock(), getExceptionListItemSchemaMock()];
|
||||
const osTypes = ['windows', 'macos'];
|
||||
const result = enrichExceptionItemsWithOS(payload, osTypes);
|
||||
const expected = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
_tags: [...getExceptionListItemSchemaMock()._tags, 'os:windows', 'os:macos'],
|
||||
},
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
_tags: [...getExceptionListItemSchemaMock()._tags, 'os:windows', 'os:macos'],
|
||||
},
|
||||
];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it should add os tag to all exception items without duplication', () => {
|
||||
const payload = [
|
||||
{ ...getExceptionListItemSchemaMock(), _tags: ['os:linux', 'os:windows'] },
|
||||
{ ...getExceptionListItemSchemaMock(), _tags: ['os:linux'] },
|
||||
];
|
||||
const osTypes = ['windows'];
|
||||
const result = enrichExceptionItemsWithOS(payload, osTypes);
|
||||
const expected = [
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
_tags: ['os:linux', 'os:windows'],
|
||||
},
|
||||
{
|
||||
...getExceptionListItemSchemaMock(),
|
||||
_tags: ['os:linux', 'os:windows'],
|
||||
},
|
||||
];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
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: OperatorTypeEnum.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 IIndexPattern;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,7 +14,6 @@ import * as i18n from './translations';
|
|||
import {
|
||||
FormattedEntry,
|
||||
BuilderEntry,
|
||||
EmptyListEntry,
|
||||
DescriptionListItem,
|
||||
FormattedBuilderEntry,
|
||||
CreateExceptionListItemBuilderSchema,
|
||||
|
@ -39,9 +38,6 @@ import {
|
|||
ExceptionListType,
|
||||
} from '../../../lists_plugin_deps';
|
||||
import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
|
||||
export const isListType = (item: BuilderEntry): item is EmptyListEntry =>
|
||||
item.type === OperatorTypeEnum.LIST;
|
||||
import { TimelineNonEcsData } from '../../../graphql/types';
|
||||
import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard';
|
||||
|
||||
|
@ -82,11 +78,6 @@ export const getExceptionOperatorSelect = (item: BuilderEntry): OperatorOption =
|
|||
}
|
||||
};
|
||||
|
||||
export const getExceptionOperatorFromSelect = (value: string): OperatorOption => {
|
||||
const operator = EXCEPTION_OPERATORS.filter(({ message }) => message === value);
|
||||
return operator[0] ?? isOperator;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats ExceptionItem entries into simple field, operator, value
|
||||
* for use in rendering items in table
|
||||
|
@ -158,19 +149,32 @@ export const formatEntry = ({
|
|||
};
|
||||
};
|
||||
|
||||
export const getOperatingSystems = (tags: string[]): string => {
|
||||
const osMatches = tags
|
||||
.filter((tag) => tag.startsWith('os:'))
|
||||
.map((os) => capitalize(os.substring(3).trim()))
|
||||
.join(', ');
|
||||
|
||||
return osMatches;
|
||||
};
|
||||
|
||||
export const getOsTagValues = (tags: string[]): string[] => {
|
||||
/**
|
||||
* Retrieves the values of tags marked as os
|
||||
*
|
||||
* @param tags an ExceptionItem's tags
|
||||
*/
|
||||
export const getOperatingSystems = (tags: string[]): string[] => {
|
||||
return tags.filter((tag) => tag.startsWith('os:')).map((os) => os.substring(3).trim());
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats os value array to a displayable string
|
||||
*/
|
||||
export const formatOperatingSystems = (osTypes: string[]): string => {
|
||||
return osTypes
|
||||
.map((os) => {
|
||||
if (os === 'macos') {
|
||||
return 'macOS';
|
||||
}
|
||||
return capitalize(os);
|
||||
})
|
||||
.join(', ');
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all tags that match a given regex
|
||||
*/
|
||||
export const getTagsInclude = ({
|
||||
tags,
|
||||
regex,
|
||||
|
@ -194,7 +198,7 @@ export const getDescriptionListContent = (
|
|||
const details = [
|
||||
{
|
||||
title: i18n.OPERATING_SYSTEM,
|
||||
value: getOperatingSystems(exceptionItem._tags),
|
||||
value: formatOperatingSystems(getOperatingSystems(exceptionItem._tags ?? [])),
|
||||
},
|
||||
{
|
||||
title: i18n.DATE_CREATED,
|
||||
|
@ -376,6 +380,11 @@ export const formatExceptionItemForUpdate = (
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds new and existing comments to all new exceptionItems if not present already
|
||||
* @param exceptionItems new or existing ExceptionItem[]
|
||||
* @param comments new Comments
|
||||
*/
|
||||
export const enrichExceptionItemsWithComments = (
|
||||
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>,
|
||||
comments: Array<Comments | CreateComments>
|
||||
|
@ -388,6 +397,11 @@ export const enrichExceptionItemsWithComments = (
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds provided osTypes to all exceptionItems if not present already
|
||||
* @param exceptionItems new or existing ExceptionItem[]
|
||||
* @param osTypes array of os values
|
||||
*/
|
||||
export const enrichExceptionItemsWithOS = (
|
||||
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>,
|
||||
osTypes: string[]
|
||||
|
@ -402,18 +416,21 @@ export const enrichExceptionItemsWithOS = (
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the value for the given fieldname within TimelineNonEcsData if it exists
|
||||
*/
|
||||
export const getMappedNonEcsValue = ({
|
||||
data,
|
||||
fieldName,
|
||||
}: {
|
||||
data: TimelineNonEcsData[];
|
||||
fieldName: string;
|
||||
}): string[] | undefined => {
|
||||
}): string[] => {
|
||||
const item = data.find((d) => d.field === fieldName);
|
||||
if (item != null && item.value != null) {
|
||||
return item.value;
|
||||
}
|
||||
return undefined;
|
||||
return [];
|
||||
};
|
||||
|
||||
export const entryHasListType = (
|
||||
|
@ -421,7 +438,7 @@ export const entryHasListType = (
|
|||
) => {
|
||||
for (const { entries } of exceptionItems) {
|
||||
for (const exceptionEntry of entries ?? []) {
|
||||
if (getOperatorType(exceptionEntry) === 'list') {
|
||||
if (getOperatorType(exceptionEntry) === OperatorTypeEnum.LIST) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -429,16 +446,29 @@ export const entryHasListType = (
|
|||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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>,
|
||||
indexPatterns: IIndexPattern
|
||||
): boolean => {
|
||||
const doesFieldNameExist = (exceptionEntry: Entry): 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 (indexPatterns.fields.find(({ name }) => name === exceptionEntry.field) === undefined) {
|
||||
if (exceptionEntry.type === 'nested') {
|
||||
for (const nestedExceptionEntry of exceptionEntry.entries) {
|
||||
if (doesFieldNameExist(nestedExceptionEntry) === false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (doesFieldNameExist(exceptionEntry) === false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -446,19 +476,25 @@ export const entryHasNonEcsType = (
|
|||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the default values from the alert data to autofill new endpoint exceptions
|
||||
*/
|
||||
export const defaultEndpointExceptionItems = (
|
||||
listType: ExceptionListType,
|
||||
listId: string,
|
||||
ruleName: string,
|
||||
alertData: TimelineNonEcsData[]
|
||||
): ExceptionsBuilderExceptionItem[] => {
|
||||
const [filePath] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.path' }) ?? [];
|
||||
const [signatureSigner] =
|
||||
getMappedNonEcsValue({ data: alertData, fieldName: 'file.Ext.code_signature.subject_name' }) ??
|
||||
[];
|
||||
const [signatureTrusted] =
|
||||
getMappedNonEcsValue({ data: alertData, fieldName: 'file.Ext.code_signature.trusted' }) ?? [];
|
||||
const [sha1Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha1' }) ?? [];
|
||||
const [filePath] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.path' });
|
||||
const [signatureSigner] = getMappedNonEcsValue({
|
||||
data: alertData,
|
||||
fieldName: 'file.Ext.code_signature.subject_name',
|
||||
});
|
||||
const [signatureTrusted] = getMappedNonEcsValue({
|
||||
data: alertData,
|
||||
fieldName: 'file.Ext.code_signature.trusted',
|
||||
});
|
||||
const [sha1Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha1' });
|
||||
const namespaceType = 'agnostic';
|
||||
|
||||
return [
|
||||
|
@ -483,7 +519,7 @@ export const defaultEndpointExceptionItems = (
|
|||
value: signatureSigner ?? '',
|
||||
},
|
||||
{
|
||||
field: 'file.code_signature.trusted',
|
||||
field: 'file.Ext.code_signature.trusted',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: signatureTrusted ?? '',
|
||||
|
@ -508,7 +544,7 @@ export const defaultEndpointExceptionItems = (
|
|||
field: 'event.category',
|
||||
operator: 'included',
|
||||
type: 'match_any',
|
||||
value: getMappedNonEcsValue({ data: alertData, fieldName: 'event.category' }) ?? [],
|
||||
value: getMappedNonEcsValue({ data: alertData, fieldName: 'event.category' }),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -184,7 +184,11 @@ const ExceptionsViewerComponent = ({
|
|||
[setCurrentModal]
|
||||
);
|
||||
|
||||
const handleCloseExceptionModal = useCallback((): void => {
|
||||
const handleOnCancelExceptionModal = useCallback((): void => {
|
||||
setCurrentModal(null);
|
||||
}, [setCurrentModal]);
|
||||
|
||||
const handleOnConfirmExceptionModal = useCallback((): void => {
|
||||
setCurrentModal(null);
|
||||
handleFetchList();
|
||||
}, [setCurrentModal, handleFetchList]);
|
||||
|
@ -255,8 +259,8 @@ const ExceptionsViewerComponent = ({
|
|||
ruleName={ruleName}
|
||||
exceptionListType={exceptionListTypeToEdit}
|
||||
exceptionItem={exceptionToEdit}
|
||||
onCancel={handleCloseExceptionModal}
|
||||
onConfirm={handleCloseExceptionModal}
|
||||
onCancel={handleOnCancelExceptionModal}
|
||||
onConfirm={handleOnConfirmExceptionModal}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -265,8 +269,8 @@ const ExceptionsViewerComponent = ({
|
|||
ruleName={ruleName}
|
||||
ruleId={ruleId}
|
||||
exceptionListType={exceptionListTypeToEdit}
|
||||
onCancel={handleCloseExceptionModal}
|
||||
onConfirm={handleCloseExceptionModal}
|
||||
onCancel={handleOnCancelExceptionModal}
|
||||
onConfirm={handleOnConfirmExceptionModal}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ import {
|
|||
SetEventsLoadingProps,
|
||||
UpdateTimelineLoading,
|
||||
} from './types';
|
||||
import { Ecs } from '../../../graphql/types';
|
||||
import { Ecs, TimelineNonEcsData } from '../../../graphql/types';
|
||||
import { AddExceptionOnClick } from '../../../common/components/exceptions/add_exception_modal';
|
||||
import { getMappedNonEcsValue } from '../../../common/components/exceptions/helpers';
|
||||
|
||||
|
@ -174,6 +174,8 @@ export const requiredFieldsForActions = [
|
|||
'signal.rule.query',
|
||||
'signal.rule.to',
|
||||
'signal.rule.id',
|
||||
'signal.original_event.kind',
|
||||
'signal.original_event.module',
|
||||
|
||||
// Endpoint exception fields
|
||||
'file.path',
|
||||
|
@ -189,6 +191,7 @@ interface AlertActionArgs {
|
|||
createTimeline: CreateTimeline;
|
||||
dispatch: Dispatch;
|
||||
ecsRowData: Ecs;
|
||||
nonEcsRowData: TimelineNonEcsData[];
|
||||
hasIndexWrite: boolean;
|
||||
onAlertStatusUpdateFailure: (status: Status, error: Error) => void;
|
||||
onAlertStatusUpdateSuccess: (count: number, status: Status) => void;
|
||||
|
@ -211,6 +214,7 @@ export const getAlertActions = ({
|
|||
createTimeline,
|
||||
dispatch,
|
||||
ecsRowData,
|
||||
nonEcsRowData,
|
||||
hasIndexWrite,
|
||||
onAlertStatusUpdateFailure,
|
||||
onAlertStatusUpdateSuccess,
|
||||
|
@ -281,6 +285,18 @@ export const getAlertActions = ({
|
|||
width: DEFAULT_ICON_BUTTON_WIDTH,
|
||||
};
|
||||
|
||||
const isEndpointAlert = () => {
|
||||
const [module] = getMappedNonEcsValue({
|
||||
data: nonEcsRowData,
|
||||
fieldName: 'signal.original_event.module',
|
||||
});
|
||||
const [kind] = getMappedNonEcsValue({
|
||||
data: nonEcsRowData,
|
||||
fieldName: 'signal.original_event.kind',
|
||||
});
|
||||
return module === 'endpoint' && kind === 'alert';
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
...getInvestigateInResolverAction({ dispatch, timelineId }),
|
||||
|
@ -305,15 +321,14 @@ export const getAlertActions = ({
|
|||
...(FILTER_OPEN !== status ? [openAlertActionComponent] : []),
|
||||
...(FILTER_CLOSED !== status ? [closeAlertActionComponent] : []),
|
||||
...(FILTER_IN_PROGRESS !== status ? [inProgressAlertActionComponent] : []),
|
||||
// TODO: disable this option if the alert is not an Endpoint alert
|
||||
{
|
||||
onClick: ({ ecsData, data }: TimelineRowActionOnClick) => {
|
||||
const ruleNameValue = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' });
|
||||
const ruleId = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' });
|
||||
if (ruleId !== undefined && ruleId.length > 0) {
|
||||
const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' });
|
||||
const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' });
|
||||
if (ruleId !== undefined) {
|
||||
openAddExceptionModal({
|
||||
ruleName: ruleNameValue ? ruleNameValue[0] : '',
|
||||
ruleId: ruleId[0],
|
||||
ruleName: ruleName ?? '',
|
||||
ruleId,
|
||||
exceptionListType: 'endpoint',
|
||||
alertData: {
|
||||
ecsData,
|
||||
|
@ -323,7 +338,7 @@ export const getAlertActions = ({
|
|||
}
|
||||
},
|
||||
id: 'addEndpointException',
|
||||
isActionDisabled: () => !canUserCRUD || !hasIndexWrite,
|
||||
isActionDisabled: () => !canUserCRUD || !hasIndexWrite || !isEndpointAlert(),
|
||||
dataTestSubj: 'add-endpoint-exception-menu-item',
|
||||
ariaLabel: 'Add Endpoint Exception',
|
||||
content: <EuiText size="m">{i18n.ACTION_ADD_ENDPOINT_EXCEPTION}</EuiText>,
|
||||
|
@ -331,12 +346,12 @@ export const getAlertActions = ({
|
|||
},
|
||||
{
|
||||
onClick: ({ ecsData, data }: TimelineRowActionOnClick) => {
|
||||
const ruleNameValue = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' });
|
||||
const ruleId = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' });
|
||||
if (ruleId !== undefined && ruleId.length > 0) {
|
||||
const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' });
|
||||
const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' });
|
||||
if (ruleId !== undefined) {
|
||||
openAddExceptionModal({
|
||||
ruleName: ruleNameValue ? ruleNameValue[0] : '',
|
||||
ruleId: ruleId[0],
|
||||
ruleName: ruleName ?? '',
|
||||
ruleId,
|
||||
exceptionListType: 'detection',
|
||||
alertData: {
|
||||
ecsData,
|
||||
|
|
|
@ -22,7 +22,10 @@ import { inputsSelectors, State, inputsModel } from '../../../common/store';
|
|||
import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline';
|
||||
import { TimelineModel } from '../../../timelines/store/timeline/model';
|
||||
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
|
||||
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
|
||||
import {
|
||||
useManageTimeline,
|
||||
TimelineRowActionArgs,
|
||||
} from '../../../timelines/components/manage_timeline';
|
||||
import { useApolloClient } from '../../../common/utils/apollo_context';
|
||||
|
||||
import { updateAlertStatusAction } from './actions';
|
||||
|
@ -48,7 +51,6 @@ import {
|
|||
displaySuccessToast,
|
||||
displayErrorToast,
|
||||
} from '../../../common/components/toasters';
|
||||
import { Ecs } from '../../../graphql/types';
|
||||
import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers';
|
||||
import {
|
||||
AddExceptionModal,
|
||||
|
@ -321,12 +323,13 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
|
||||
// Send to Timeline / Update Alert Status Actions for each table row
|
||||
const additionalActions = useMemo(
|
||||
() => (ecsRowData: Ecs) =>
|
||||
() => ({ ecsData, nonEcsData }: TimelineRowActionArgs) =>
|
||||
getAlertActions({
|
||||
apolloClient,
|
||||
canUserCRUD,
|
||||
createTimeline: createTimelineCallback,
|
||||
ecsRowData,
|
||||
ecsRowData: ecsData,
|
||||
nonEcsRowData: nonEcsData,
|
||||
dispatch,
|
||||
hasIndexWrite,
|
||||
onAlertStatusUpdateFailure,
|
||||
|
@ -401,9 +404,12 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
closeAddExceptionModal();
|
||||
}, [closeAddExceptionModal]);
|
||||
|
||||
const onAddExceptionConfirm = useCallback(() => {
|
||||
closeAddExceptionModal();
|
||||
}, [closeAddExceptionModal]);
|
||||
const onAddExceptionConfirm = useCallback(
|
||||
(didCloseAlert: boolean) => {
|
||||
closeAddExceptionModal();
|
||||
},
|
||||
[closeAddExceptionModal]
|
||||
);
|
||||
|
||||
if (loading || isEmpty(signalsIndex)) {
|
||||
return (
|
||||
|
|
|
@ -14,7 +14,7 @@ import { SubsetTimelineModel } from '../../store/timeline/model';
|
|||
import * as i18n from '../../../common/components/events_viewer/translations';
|
||||
import * as i18nF from '../timeline/footer/translations';
|
||||
import { timelineDefaults as timelineDefaultModel } from '../../store/timeline/defaults';
|
||||
import { Ecs } from '../../../graphql/types';
|
||||
import { Ecs, TimelineNonEcsData } from '../../../graphql/types';
|
||||
|
||||
interface ManageTimelineInit {
|
||||
documentType?: string;
|
||||
|
@ -25,11 +25,16 @@ interface ManageTimelineInit {
|
|||
indexToAdd?: string[] | null;
|
||||
loadingText?: string;
|
||||
selectAll?: boolean;
|
||||
timelineRowActions: (ecsData: Ecs) => TimelineRowAction[];
|
||||
timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[];
|
||||
title?: string;
|
||||
unit?: (totalCount: number) => string;
|
||||
}
|
||||
|
||||
export interface TimelineRowActionArgs {
|
||||
ecsData: Ecs;
|
||||
nonEcsData: TimelineNonEcsData[];
|
||||
}
|
||||
|
||||
interface ManageTimeline {
|
||||
documentType: string;
|
||||
defaultModel: SubsetTimelineModel;
|
||||
|
@ -41,7 +46,7 @@ interface ManageTimeline {
|
|||
loadingText: string;
|
||||
queryFields: string[];
|
||||
selectAll: boolean;
|
||||
timelineRowActions: (ecsData: Ecs) => TimelineRowAction[];
|
||||
timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[];
|
||||
title: string;
|
||||
unit: (totalCount: number) => string;
|
||||
}
|
||||
|
@ -71,7 +76,7 @@ type ActionManageTimeline =
|
|||
id: string;
|
||||
payload: {
|
||||
queryFields?: string[];
|
||||
timelineRowActions: (ecsData: Ecs) => TimelineRowAction[];
|
||||
timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[];
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -142,7 +147,7 @@ interface UseTimelineManager {
|
|||
setTimelineRowActions: (actionsArgs: {
|
||||
id: string;
|
||||
queryFields?: string[];
|
||||
timelineRowActions: (ecsData: Ecs) => TimelineRowAction[];
|
||||
timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
|
@ -167,7 +172,7 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT
|
|||
}: {
|
||||
id: string;
|
||||
queryFields?: string[];
|
||||
timelineRowActions: (ecsData: Ecs) => TimelineRowAction[];
|
||||
timelineRowActions: ({ ecsData, nonEcsData }: TimelineRowActionArgs) => TimelineRowAction[];
|
||||
}) => {
|
||||
dispatch({
|
||||
type: 'SET_TIMELINE_ACTIONS',
|
||||
|
|
|
@ -90,8 +90,8 @@ export const EventColumnView = React.memo<Props>(
|
|||
}) => {
|
||||
const { getManageTimelineById } = useManageTimeline();
|
||||
const timelineActions = useMemo(
|
||||
() => getManageTimelineById(timelineId).timelineRowActions(ecsData),
|
||||
[ecsData, getManageTimelineById, timelineId]
|
||||
() => getManageTimelineById(timelineId).timelineRowActions({ nonEcsData: data, ecsData }),
|
||||
[data, ecsData, getManageTimelineById, timelineId]
|
||||
);
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ import { Sort } from './sort';
|
|||
import { useManageTimeline } from '../../manage_timeline';
|
||||
import { GraphOverlay } from '../../graph_overlay';
|
||||
import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
|
||||
import { TimelineRowAction } from './actions';
|
||||
|
||||
export interface BodyProps {
|
||||
addNoteToEvent: AddNoteToEvent;
|
||||
|
@ -104,7 +105,18 @@ export const Body = React.memo<BodyProps>(
|
|||
const containerElementRef = useRef<HTMLDivElement>(null);
|
||||
const { getManageTimelineById } = useManageTimeline();
|
||||
const timelineActions = useMemo(
|
||||
() => (data.length > 0 ? getManageTimelineById(id).timelineRowActions(data[0].ecs) : []),
|
||||
() =>
|
||||
data.reduce((acc: TimelineRowAction[], rowData) => {
|
||||
const rowActions = getManageTimelineById(id).timelineRowActions({
|
||||
ecsData: rowData.ecs,
|
||||
nonEcsData: rowData.data,
|
||||
});
|
||||
return rowActions &&
|
||||
rowActions.filter((v) => v.displayType === 'icon').length >
|
||||
acc.filter((v) => v.displayType === 'icon').length
|
||||
? rowActions
|
||||
: acc;
|
||||
}, []),
|
||||
[data, getManageTimelineById, id]
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue