[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:
Davis Plumlee 2020-07-09 20:59:46 -04:00 committed by GitHub
parent 87c8de8c7d
commit c1b26651bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 430 additions and 80 deletions

View file

@ -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) {

View file

@ -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}

View file

@ -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:",
}

View file

@ -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);
});
});
});

View file

@ -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' }),
},
],
},

View file

@ -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}
/>
)}

View file

@ -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,

View file

@ -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 (

View file

@ -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',

View file

@ -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);

View file

@ -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]
);