[Security Solution] [Exceptions] Auto-populate exception flyout with alert’s “highlighted fields” values (#159029)

## Summary

-  Addresses https://github.com/elastic/security-team/issues/6405

**Contents of this PR:**

- Exports the `getEventFieldsToDisplay` function from the AlertSummary
component, which retrieves the Highlighted Fields based on the Event
data and Rule
[get_alert_summary_rows.tsx](https://github.com/elastic/kibana/compare/main...WafaaNasr:kibana:6405-autopopulate-rule-exception-with-highlightedfields?expand=1#diff-6e544b31b8762fba87b411f14aa58742469ec5a9c62249ae8d030d2c62e6b023)
- Introduces helper functions to populate the highlighted fields from
the `alertData` in the
[add_exception_flyout](https://github.com/elastic/kibana/compare/main...WafaaNasr:kibana:6405-autopopulate-rule-exception-with-highlightedfields?expand=1#diff-10f41a69a528b4fee8e9c3a0308519b55d34e6a8d7bf5a245113a59477883e5a)
component.
- Adds
[highlighted_fields_config.ts](https://github.com/elastic/kibana/compare/main...WafaaNasr:kibana:6405-autopopulate-rule-exception-with-highlightedfields?expand=1#diff-70943899e3414daa47b9cc1e308f9556d9f4ceb3d537013d4b0f078a35f88735)
configuration file, which contains the fields to be filtered out from
the Exceptions and the fields used to obtain the highlighted fields.
- Auto-populate the `Rule Exception`
[add_exception_flyout](https://github.com/elastic/kibana/compare/main...WafaaNasr:kibana:6405-autopopulate-rule-exception-with-highlightedfields?expand=1#diff-10f41a69a528b4fee8e9c3a0308519b55d34e6a8d7bf5a245113a59477883e5a)
on initiation, if alertData is provided and listType is `RuleException`,
with the highlighted fields from the Alert.

## Screenshots



![image](9872d8fb-9818-4b21-913c-353bc165812f)


**Checklist**

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Wafaa Nasr 2023-06-15 16:16:53 +01:00 committed by GitHub
parent a13ac775b3
commit 9ebe5d5f53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 732 additions and 46 deletions

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { deleteAlertsAndRules } from '../../../tasks/common';
import {
goToClosedAlertsOnRuleDetailsPage,
goToOpenedAlertsOnRuleDetailsPage,
@ -28,13 +29,10 @@ import {
addExceptionFlyoutItemName,
selectCloseSingleAlerts,
submitNewExceptionItem,
validateExceptionConditionField,
} from '../../../tasks/exceptions';
import { ALERTS_COUNT, EMPTY_ALERT_TABLE } from '../../../screens/alerts';
import {
EXCEPTION_ITEM_CONTAINER,
FIELD_INPUT_PARENT,
NO_EXCEPTIONS_EXIST_PROMPT,
} from '../../../screens/exceptions';
import { NO_EXCEPTIONS_EXIST_PROMPT } from '../../../screens/exceptions';
import {
removeException,
goToAlertsTab,
@ -43,10 +41,13 @@ import {
describe('Endpoint Exceptions workflows from Alert', () => {
const expectedNumberOfAlerts = 1;
beforeEach(() => {
before(() => {
esArchiverResetKibana();
esArchiverLoad('endpoint');
});
beforeEach(() => {
login();
deleteAlertsAndRules();
esArchiverLoad('endpoint');
createRule(getEndpointRule());
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
goToRuleDetails();
@ -64,12 +65,8 @@ describe('Endpoint Exceptions workflows from Alert', () => {
openAddEndpointExceptionFromFirstAlert();
// As the endpoint.alerts-* is used to trigger the alert the
// file.Ext.code_signature will be populated as the first item
cy.get(EXCEPTION_ITEM_CONTAINER)
.eq(0)
.find(FIELD_INPUT_PARENT)
.eq(0)
.should('have.text', 'file.Ext.code_signature');
// file.Ext.code_signature will be auto-populated
validateExceptionConditionField('file.Ext.code_signature');
selectCloseSingleAlerts();
addExceptionFlyoutItemName('Sample Exception');

View file

@ -6,7 +6,7 @@
*/
import { LOADING_INDICATOR } from '../../../screens/security_header';
import { getNewRule } from '../../../objects/rule';
import { getNewRule, getEndpointRule } from '../../../objects/rule';
import { ALERTS_COUNT, EMPTY_ALERT_TABLE } from '../../../screens/alerts';
import { createRule } from '../../../tasks/api_calls/rules';
import { goToRuleDetails } from '../../../tasks/alerts_detection_rules';
@ -24,6 +24,8 @@ import {
submitNewExceptionItem,
validateExceptionItemFirstAffectedRuleNameInRulePage,
validateExceptionItemAffectsTheCorrectRulesInRulePage,
validateExceptionConditionField,
validateExceptionCommentCountAndText,
} from '../../../tasks/exceptions';
import {
esArchiverLoad,
@ -47,19 +49,22 @@ describe('Rule Exceptions workflows from Alert', () => {
const EXPECTED_NUMBER_OF_ALERTS = '1 alert';
const ITEM_NAME = 'Sample Exception List Item';
const newRule = getNewRule();
before(() => {
esArchiverResetKibana();
esArchiverLoad('exceptions');
login();
postDataView('exceptions-*');
});
beforeEach(() => {
esArchiverResetKibana();
deleteAlertsAndRules();
});
after(() => {
esArchiverUnload('exceptions');
});
afterEach(() => {
esArchiverUnload('exceptions_2');
});
beforeEach(() => {
deleteAlertsAndRules();
it('Creates an exception item from alert actions overflow menu and close all matching alerts', () => {
esArchiverLoad('exceptions');
login();
postDataView('exceptions-*');
createRule({
...newRule,
query: 'agent.name:*',
@ -70,13 +75,7 @@ describe('Rule Exceptions workflows from Alert', () => {
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
goToRuleDetails();
waitForAlertsToPopulate();
});
afterEach(() => {
esArchiverUnload('exceptions_2');
});
it('Creates an exception item from alert actions overflow menu and close all matching alerts', () => {
cy.get(LOADING_INDICATOR).should('not.exist');
addExceptionFromFirstAlert();
@ -120,4 +119,45 @@ describe('Rule Exceptions workflows from Alert', () => {
cy.get(ALERTS_COUNT).should('have.text', '2 alerts');
});
it('Creates an exception item from alert actions overflow menu and auto populate the conditions using alert Highlighted fields ', () => {
esArchiverLoad('endpoint');
login();
createRule(getEndpointRule());
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
goToRuleDetails();
waitForAlertsToPopulate();
cy.get(LOADING_INDICATOR).should('not.exist');
addExceptionFromFirstAlert();
const highlightedFieldsBasedOnAlertDoc = [
'host.name',
'agent.id',
'user.name',
'process.executable',
'file.path',
];
/**
* Validate the highlighted fields are auto populated, these
* fields are based on the alert document that should be generated
* when the endpoint rule runs
*/
highlightedFieldsBasedOnAlertDoc.forEach((field, index) => {
validateExceptionConditionField(field);
});
/**
* Validate that the comments are opened by default with one comment added
* showing a text contains information about the pre-filled conditions
*/
validateExceptionCommentCountAndText(
1,
'Exception conditions are pre-filled with relevant data from'
);
addExceptionFlyoutItemName(ITEM_NAME);
submitNewExceptionItem();
});
});

View file

@ -164,6 +164,9 @@ export const addExceptionConditions = (exception: Exception) => {
});
};
export const validateExceptionConditionField = (value: string) => {
cy.get(EXCEPTION_ITEM_CONTAINER).contains('span', value);
};
export const submitNewExceptionItem = () => {
cy.get(CONFIRM_BTN).click();
cy.get(CONFIRM_BTN).should('not.exist');
@ -193,14 +196,13 @@ export const selectOs = (os: string) => {
export const addExceptionComment = (comment: string) => {
cy.get(EXCEPTION_COMMENTS_ACCORDION_BTN).click();
cy.get(EXCEPTION_COMMENT_TEXT_AREA).type(`${comment}`);
// cy.root()
// .pipe(($el) => {
// return $el.find(EXCEPTION_COMMENT_TEXT_AREA);
// })
// .clear()
// .type(`${comment}`)
cy.get(EXCEPTION_COMMENT_TEXT_AREA).should('have.value', comment);
};
export const validateExceptionCommentCountAndText = (count: number, comment: string) => {
cy.get(EXCEPTION_COMMENTS_ACCORDION_BTN).contains('h3', count);
cy.get(EXCEPTION_COMMENT_TEXT_AREA).contains('textarea', comment);
};
export const clickOnShowComments = () => {
cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER_SHOW_COMMENTS_BTN).click();
};

View file

@ -215,10 +215,17 @@ function getFieldsByRuleType(ruleType?: string): EventSummaryField[] {
}
}
/**
This function is exported because it is used in the Exception Component to
populate the conditions with the Highlighted Fields. Additionally, the new
Alert Summary Flyout also requires access to these fields.
As the Alert Summary components will undergo changes soon we will go with
exporting the function only for now.
*/
/**
* Assembles a list of fields to display based on the event
*/
function getEventFieldsToDisplay({
export function getEventFieldsToDisplay({
eventCategories,
eventCode,
eventRuleType,

View file

@ -8,11 +8,11 @@
import React from 'react';
import type { ReactWrapper } from 'enzyme';
import { mount, shallow } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { waitFor, render } from '@testing-library/react';
import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock';
import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public';
import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types';
import type { EntriesArray, EntryMatch } from '@kbn/securitysolution-io-ts-list-types';
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
import { createStubIndexPattern, stubIndexPattern } from '@kbn/data-plugin/common/stubs';
@ -612,6 +612,58 @@ describe('When the add exception modal is opened', () => {
});
});
describe('Auto populate rule exception', () => {
beforeEach(() => {
mockGetExceptionBuilderComponentLazy.mockImplementation((props) => {
return (
<span data-test-subj="alertExceptionBuilder">
{props.exceptionListItems &&
props.exceptionListItems[0] &&
props.exceptionListItems[0].entries.map(
({ field, operator, type, value }: EntryMatch) => (
<>
<span data-test-subj="entryField">{field} </span>
<span data-test-subj="entryOperator">{operator} </span>
<span data-test-subj="entryType">{type} </span>
<span data-test-subj="entryValue">{value} </span>
</>
)
)}
</span>
);
});
});
it('should auto populate the exception from alert highlighted fields', () => {
const wrapper = render(
(() => (
<TestProviders>
<AddExceptionFlyout
rules={[
{
...getRulesSchemaMock(),
exceptions_list: [],
} as Rule,
]}
isBulkAction={false}
alertData={alertDataMock}
isAlertDataLoading={false}
alertStatus="open"
isEndpointItem={false}
showAlertCloseOptions
onCancel={jest.fn()}
onConfirm={jest.fn()}
/>
</TestProviders>
))()
);
const { getByTestId } = wrapper;
expect(getByTestId('alertExceptionBuilder')).toBeInTheDocument();
expect(getByTestId('entryField')).toHaveTextContent('file.path');
expect(getByTestId('entryOperator')).toHaveTextContent('included');
expect(getByTestId('entryType')).toHaveTextContent('match');
expect(getByTestId('entryValue')).toHaveTextContent('test/path');
});
});
describe('bulk closeable alert data is passed in', () => {
let wrapper: ReactWrapper;
beforeEach(async () => {

View file

@ -41,6 +41,7 @@ import {
defaultEndpointExceptionItems,
retrieveAlertOsTypes,
filterIndexPatterns,
getPrepopulatedRuleExceptionWithHighlightFields,
} from '../../utils/helpers';
import type { AlertData } from '../../utils/types';
import { initialState, createExceptionItemsReducer } from './reducer';
@ -335,12 +336,26 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
);
useEffect((): void => {
if (listType === ExceptionListTypeEnum.ENDPOINT && alertData != null) {
setInitialExceptionItems(
defaultEndpointExceptionItems(ENDPOINT_LIST_ID, exceptionItemName, alertData)
);
if (alertData) {
switch (listType) {
case ExceptionListTypeEnum.ENDPOINT: {
return setInitialExceptionItems(
defaultEndpointExceptionItems(ENDPOINT_LIST_ID, exceptionItemName, alertData)
);
}
case ExceptionListTypeEnum.RULE_DEFAULT: {
const populatedException = getPrepopulatedRuleExceptionWithHighlightFields({
alertData,
exceptionItemName,
});
if (populatedException) {
setComment(i18n.ADD_RULE_EXCEPTION_FROM_ALERT_COMMENT(alertData._id));
return setInitialExceptionItems([populatedException]);
}
}
}
}
}, [listType, exceptionItemName, alertData, setInitialExceptionItems]);
}, [listType, exceptionItemName, alertData, setInitialExceptionItems, setComment]);
const osTypesSelection = useMemo((): OsTypeArray => {
return hasAlertData ? retrieveAlertOsTypes(alertData) : selectedOs ? [...selectedOs] : [];
@ -521,9 +536,10 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
<ExceptionItemComments
accordionTitle={
<SectionHeader size="xs">
<h3>{i18n.COMMENTS_SECTION_TITLE(0)}</h3>
<h3>{i18n.COMMENTS_SECTION_TITLE(newComment ? 1 : 0)}</h3>
</SectionHeader>
}
initialIsOpen={!!newComment}
newCommentValue={newComment}
newCommentOnChange={setComment}
/>

View file

@ -83,3 +83,12 @@ export const COMMENTS_SECTION_TITLE = (comments: number) =>
values: { comments },
defaultMessage: 'Add comments ({comments})',
});
export const ADD_RULE_EXCEPTION_FROM_ALERT_COMMENT = (alertId: string) =>
i18n.translate(
'xpack.securitySolution.ruleExceptions.addExceptionFlyout.addRuleExceptionFromAlertComment',
{
values: { alertId },
defaultMessage: 'Exception conditions are pre-filled with relevant data from {alertId}.',
}
);

View file

@ -26,6 +26,7 @@ interface ExceptionItemCommentsProps {
exceptionItemComments?: Comment[];
newCommentValue: string;
accordionTitle?: JSX.Element;
initialIsOpen?: boolean;
newCommentOnChange: (value: string) => void;
}
@ -50,6 +51,7 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({
exceptionItemComments,
newCommentValue,
accordionTitle,
initialIsOpen = false,
newCommentOnChange,
}: ExceptionItemCommentsProps) {
const [shouldShowComments, setShouldShowComments] = useState(false);
@ -107,6 +109,7 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({
return (
<div>
<CommentAccordion
initialIsOpen={initialIsOpen}
id={'add-exception-comments-accordion'}
buttonClassName={COMMENT_ACCORDION_BUTTON_CLASS_NAME}
buttonContent={accordionTitle ?? commentsAccordionTitle}

View file

@ -25,13 +25,20 @@ import {
retrieveAlertOsTypes,
filterIndexPatterns,
getCodeSignatureValue,
buildRuleExceptionWithConditions,
buildExceptionEntriesFromAlertFields,
filterHighlightedFields,
getPrepopulatedRuleExceptionWithHighlightFields,
getAlertHighlightedFields,
} from './helpers';
import * as mockHelpers from './helpers';
import type { AlertData, Flattened } from './types';
import type {
EntriesArray,
OsTypeArray,
ExceptionListItemSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import { ListOperatorTypeEnum, ListOperatorEnum } from '@kbn/securitysolution-io-ts-list-types';
import type { DataViewBase } from '@kbn/es-query';
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
@ -44,7 +51,6 @@ import {
ALERT_ORIGINAL_EVENT_KIND,
ALERT_ORIGINAL_EVENT_MODULE,
} from '../../../../common/field_maps/field_names';
jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('123'),
}));
@ -1472,4 +1478,415 @@ describe('Exception helpers', () => {
]);
});
});
describe('Auto-populate Rule Exceptions with Alert highlighted fields', () => {
const name = 'Exception name';
const endpointCapabilties = [
'isolation',
'kill_process',
'suspend_process',
'running_processes',
'get_file',
'execute',
'upload_file',
];
const alertData = {
'kibana.alert.rule.category': 'Custom Query Rule',
'kibana.alert.rule.consumer': 'siem',
'kibana.alert.rule.execution.uuid': '28b687e3-8e16-48aa-91b8-bf044d366c2d',
'@timestamp': '2023-06-05T11:11:32.870Z',
agent: {
id: 'f4f86e7c-29bd-4655-b7d0-a3d08ad0c322',
type: 'endpoint',
},
process: {
parent: {
pid: 1,
},
group_leader: {
name: 'fake leader',
entity_id: 'ubi00k5f1o',
},
name: 'malware writer',
pid: 2,
entity_id: 'ycrj6wvrt4',
executable: 'C:/malware.exe',
hash: {
sha1: 'fake sha1',
sha256: 'fake sha256',
md5: 'fake md5',
},
},
'event.agent_id_status': 'auth_metadata_missing',
'event.sequence': 57,
'event.ingested': '2023-06-05T11:10:27Z',
'event.code': 'malicious_file',
'event.kind': 'signal',
'event.module': 'endpoint',
'event.action': 'deletion',
'event.id': 'c3b60ce3-6569-4136-854a-f0cc9f546339',
'event.category': 'malware',
'event.type': 'creation',
'event.dataset': 'endpoint',
'kibana.alert.rule.exceptions_list': [
{
id: 'endpoint_list',
list_id: 'endpoint_list',
namespace_type: 'agnostic',
type: 'endpoint',
},
],
Endpoint: {
capabilities: endpointCapabilties,
},
_id: 'b9edb05a090729be2077b99304542d6844973843dec43177ac618f383df44a6d',
};
const expectedHighlightedFields = [
{
id: 'host.name',
},
{
id: 'agent.id',
overrideField: 'agent.status',
label: 'Agent status',
},
{
id: 'user.name',
},
{
id: 'cloud.provider',
},
{
id: 'cloud.region',
},
{
id: 'process.executable',
},
{
id: 'process.name',
},
{
id: 'file.path',
},
{
id: 'kibana.alert.threshold_result.cardinality.field',
label: 'Event Cardinality',
},
];
const operator = ListOperatorEnum.INCLUDED;
const type = ListOperatorTypeEnum.MATCH;
const exceptionEntries: EntriesArray = [
{
field: 'host.name',
operator,
type,
value: 'Host-yxnnos4lo3',
},
{
field: 'agent.id',
operator,
type,
value: 'f4f86e7c-29bd-4655-b7d0-a3d08ad0c322',
},
{
field: 'user.name',
operator,
type,
value: 'c09uzcpj0c',
},
{
field: 'process.executable',
operator,
type,
value: 'C:/malware.exe',
},
{
field: 'file.path',
operator,
type,
value: 'C:/fake_malware.exe',
},
{
field: 'process.name',
operator,
type,
value: 'malware writer',
},
];
const defaultAlertData = {
'@timestamp': '',
_id: '',
};
const expectedExceptionEntries = [
{
field: 'agent.id',
operator: 'included',
type: 'match',
value: 'f4f86e7c-29bd-4655-b7d0-a3d08ad0c322',
},
{
field: 'process.executable',
operator: 'included',
type: 'match',
value: 'C:/malware.exe',
},
{ field: 'process.name', operator: 'included', type: 'match', value: 'malware writer' },
];
const entriesWithMatchAny = {
field: 'Endpoint.capabilities',
operator,
type: ListOperatorTypeEnum.MATCH_ANY,
value: endpointCapabilties,
};
describe('buildRuleExceptionWithConditions', () => {
it('should build conditions, name and namespace for exception correctly', () => {
const exception = buildRuleExceptionWithConditions({ name, exceptionEntries });
expect(exception.entries).toEqual(
expect.arrayContaining([
{
field: 'host.name',
id: '123',
operator: 'included',
type: 'match',
value: 'Host-yxnnos4lo3',
},
{
field: 'agent.id',
id: '123',
operator: 'included',
type: 'match',
value: 'f4f86e7c-29bd-4655-b7d0-a3d08ad0c322',
},
{
field: 'user.name',
id: '123',
operator: 'included',
type: 'match',
value: 'c09uzcpj0c',
},
{
field: 'process.executable',
id: '123',
operator: 'included',
type: 'match',
value: 'C:/malware.exe',
},
])
);
expect(exception.name).toEqual(name);
expect(exception.namespace_type).toEqual('single');
});
});
describe('buildExceptionEntriesFromAlertFields', () => {
it('should return empty entries if highlightedFields values are empty', () => {
const entries = buildExceptionEntriesFromAlertFields({ highlightedFields: [], alertData });
expect(entries).toEqual([]);
});
it('should return empty entries if alertData values are empty', () => {
const entries = buildExceptionEntriesFromAlertFields({
highlightedFields: expectedHighlightedFields,
alertData: defaultAlertData,
});
expect(entries).toEqual([]);
});
it('should build exception entries with "match" operator in case the field key has single value', () => {
const entries = buildExceptionEntriesFromAlertFields({
highlightedFields: expectedHighlightedFields,
alertData,
});
expect(entries).toEqual(expectedExceptionEntries);
});
it('should build the exception entries with "match_any" in case the field key has multiple values', () => {
const entries = buildExceptionEntriesFromAlertFields({
highlightedFields: [
...expectedHighlightedFields,
{
id: 'Endpoint.capabilities',
},
],
alertData,
});
expect(entries).toEqual([...expectedExceptionEntries, entriesWithMatchAny]);
});
});
describe('filterHighlightedFields', () => {
const prefixesToExclude = ['agent', 'cloud'];
it('should not filter any field if no prefixes passed ', () => {
const filteredFields = filterHighlightedFields(expectedHighlightedFields, []);
expect(filteredFields).toEqual(expectedHighlightedFields);
});
it('should not filter any field if no fields passed ', () => {
const filteredFields = filterHighlightedFields([], prefixesToExclude);
expect(filteredFields).toEqual([]);
});
it('should filter out the passed prefixes successfully', () => {
const filteredFields = filterHighlightedFields(
expectedHighlightedFields,
prefixesToExclude
);
expect(filteredFields).not.toEqual(
expect.arrayContaining([
{
id: 'agent.id',
overrideField: 'agent.status',
label: 'Agent status',
},
{
id: 'cloud.provider',
},
{
id: 'cloud.region',
},
])
);
});
});
describe('getAlertHighlightedFields', () => {
const baseGeneratedAlertHighlightedFields = [
{
id: 'host.name',
},
{
id: 'agent.id',
label: 'Agent status',
overrideField: 'agent.status',
},
{
id: 'user.name',
},
{
id: 'cloud.provider',
},
{
id: 'cloud.region',
},
{
id: 'orchestrator.cluster.id',
},
{
id: 'orchestrator.cluster.name',
},
{
id: 'container.image.name',
},
{
id: 'container.image.tag',
},
{
id: 'orchestrator.namespace',
},
{
id: 'orchestrator.resource.parent.type',
},
{
id: 'orchestrator.resource.type',
},
{
id: 'process.executable',
},
{
id: 'file.path',
},
];
const allHighlightFields = [
...baseGeneratedAlertHighlightedFields,
{
id: 'file.name',
},
{
id: 'file.hash.sha256',
},
{
id: 'file.directory',
},
{
id: 'process.name',
},
{
id: 'file.Ext.quarantine_path',
label: 'Quarantined file path',
overrideField: 'quarantined.path',
},
];
it('should return the highlighted fields correctly when eventCode, eventCategory and RuleType are in the alertData', () => {
const res = getAlertHighlightedFields(alertData);
expect(res).toEqual(allHighlightFields);
});
it('should return highlighted fields without the file.Ext.quarantine_path when "event.code" is not in the alertData', () => {
const alertDataWithoutEventCode = { ...alertData, 'event.code': null };
const res = getAlertHighlightedFields(alertDataWithoutEventCode);
expect(res).toEqual([
...baseGeneratedAlertHighlightedFields,
{
id: 'file.name',
},
{
id: 'file.hash.sha256',
},
{
id: 'file.directory',
},
{
id: 'process.name',
},
]);
});
it('should return highlighted fields without the file and process props when "event.category" is not in the alertData', () => {
const alertDataWithoutEventCategory = { ...alertData, 'event.category': null };
const res = getAlertHighlightedFields(alertDataWithoutEventCategory);
expect(res).toEqual([
...baseGeneratedAlertHighlightedFields,
{
id: 'file.Ext.quarantine_path',
label: 'Quarantined file path',
overrideField: 'quarantined.path',
},
]);
});
it('should return all highlighted fields even when the "kibana.alert.rule.type" is not in the alertData', () => {
const alertDataWithoutEventCategory = { ...alertData, 'kibana.alert.rule.type': null };
const res = getAlertHighlightedFields(alertDataWithoutEventCategory);
expect(res).toEqual(allHighlightFields);
});
it('should return all highlighted fields when there are no fields to be filtered out', () => {
jest.mock('./highlighted_fields_config', () => ({ highlightedFieldsPrefixToExclude: [] }));
const res = getAlertHighlightedFields(alertData);
expect(res).toEqual(allHighlightFields);
});
});
describe('getPrepopulatedRuleExceptionWithHighlightFields', () => {
it('should not create any exception and return null if there are no highlighted fields', () => {
jest.spyOn(mockHelpers, 'getAlertHighlightedFields').mockReturnValue([]);
const res = getPrepopulatedRuleExceptionWithHighlightFields({
alertData: defaultAlertData,
exceptionItemName: '',
});
expect(res).toBe(null);
});
it('should not create any exception and return null if there are exception entries generated', () => {
jest.spyOn(mockHelpers, 'buildExceptionEntriesFromAlertFields').mockReturnValue([]);
const res = getPrepopulatedRuleExceptionWithHighlightFields({
alertData: defaultAlertData,
exceptionItemName: '',
});
expect(res).toBe(null);
});
it('should create a new exception and populate its entries with the highlighted fields', () => {
const exception = getPrepopulatedRuleExceptionWithHighlightFields({
alertData,
exceptionItemName: name,
});
expect(exception?.entries).toEqual(
expectedExceptionEntries.map((entry) => ({ ...entry, id: '123' }))
);
expect(exception?.name).toEqual(name);
});
});
});
});

View file

@ -8,7 +8,7 @@
import React from 'react';
import type { EuiCommentProps } from '@elastic/eui';
import { EuiText, EuiAvatar } from '@elastic/eui';
import { capitalize, omit } from 'lodash';
import { capitalize, get, omit } from 'lodash';
import type { Moment } from 'moment';
import moment from 'moment';
@ -24,8 +24,14 @@ import type {
ExceptionListItemSchema,
UpdateExceptionListItemSchema,
ExceptionListSchema,
EntriesArray,
} from '@kbn/securitysolution-io-ts-list-types';
import {
ListOperatorTypeEnum,
ListOperatorEnum,
comment,
osType,
} from '@kbn/securitysolution-io-ts-list-types';
import { comment, osType } from '@kbn/securitysolution-io-ts-list-types';
import type {
ExceptionsBuilderExceptionItem,
@ -36,6 +42,8 @@ import type { DataViewBase } from '@kbn/es-query';
import { removeIdFromExceptionItemsEntries } from '@kbn/securitysolution-list-hooks';
import type { EcsSecurityExtension as Ecs, CodeSignature } from '@kbn/securitysolution-ecs';
import type { EventSummaryField } from '../../../common/components/event_details/types';
import { getEventFieldsToDisplay } from '../../../common/components/event_details/get_alert_summary_rows';
import * as i18n from './translations';
import type { AlertData, Flattened } from './types';
@ -45,6 +53,13 @@ import exceptionableWindowsMacFields from './exceptionable_windows_mac_fields.js
import exceptionableEndpointFields from './exceptionable_endpoint_fields.json';
import { EXCEPTIONABLE_ENDPOINT_EVENT_FIELDS } from '../../../../common/endpoint/exceptions/exceptionable_endpoint_event_fields';
import { ALERT_ORIGINAL_EVENT } from '../../../../common/field_maps/field_names';
import {
EVENT_CODE,
EVENT_CATEGORY,
getKibanaAlertIdField,
highlightedFieldsPrefixToExclude,
KIBANA_ALERT_RULE_TYPE,
} from './highlighted_fields_config';
export const filterIndexPatterns = (
patterns: DataViewBase,
@ -870,3 +885,111 @@ export const enrichSharedExceptions = (
});
});
};
/**
* Creates new Rule exception item with passed in entries
*/
export const buildRuleExceptionWithConditions = ({
name,
exceptionEntries,
}: {
name: string;
exceptionEntries: EntriesArray;
}): ExceptionsBuilderExceptionItem => {
return {
...getNewExceptionItem({ listId: undefined, namespaceType: 'single', name }),
entries: addIdToEntries(exceptionEntries),
};
};
/**
Generate exception conditions based on the highlighted fields of the alert that
have corresponding values in the alert data.
For the initial implementation the nested conditions are not considered
*/
export const buildExceptionEntriesFromAlertFields = ({
highlightedFields,
alertData,
}: {
highlightedFields: EventSummaryField[];
alertData: AlertData;
}): EntriesArray => {
return Object.values(highlightedFields).reduce((acc: EntriesArray, field) => {
const fieldKey = field.id;
const fieldValue = get(alertData, fieldKey) || get(alertData, getKibanaAlertIdField(fieldKey));
if (fieldValue) {
acc.push({
field: fieldKey,
operator: ListOperatorEnum.INCLUDED,
type: Array.isArray(fieldValue)
? ListOperatorTypeEnum.MATCH_ANY
: ListOperatorTypeEnum.MATCH,
value: fieldValue,
});
}
return acc;
}, []);
};
/**
* Prepopulate the Rule Exception with the highlighted fields from the Alert's Summary.
* @param alertData The Alert data object
* @param exceptionItemName The name of the Exception Item
* @returns A new Rule Exception Item with the highlighted fields as entries,
*/
export const getPrepopulatedRuleExceptionWithHighlightFields = ({
alertData,
exceptionItemName,
}: {
alertData: AlertData;
exceptionItemName: string;
}): ExceptionsBuilderExceptionItem | null => {
const highlightedFields = getAlertHighlightedFields(alertData);
if (!highlightedFields.length) return null;
const exceptionEntries = buildExceptionEntriesFromAlertFields({ highlightedFields, alertData });
if (!exceptionEntries.length) return null;
return buildRuleExceptionWithConditions({
name: exceptionItemName,
exceptionEntries,
});
};
/**
Filters out the irrelevant highlighted fields for Rule exceptions using
the "highlightedFieldsPrefixToExclude" array.
*/
export const filterHighlightedFields = (
fields: EventSummaryField[],
prefixesToExclude: string[]
): EventSummaryField[] => {
return fields.filter(({ id }) => {
return !prefixesToExclude.some((field: string) => id.startsWith(field));
});
};
/**
* Retrieve the highlighted fields from the Alert Summary based on the following Alert properties:
* * event.category
* * event.code
* * kibana.alert.rule.type
* @param alertData The Alert data object
*/
export const getAlertHighlightedFields = (alertData: AlertData): EventSummaryField[] => {
const eventCategory = get(alertData, EVENT_CATEGORY);
const eventCode = get(alertData, EVENT_CODE);
const eventRuleType = get(alertData, KIBANA_ALERT_RULE_TYPE);
const eventCategories = {
primaryEventCategory: eventCategory,
allEventCategories: [eventCategory],
};
const fieldsToDisplay = getEventFieldsToDisplay({
eventCategories,
eventCode,
eventRuleType,
});
return filterHighlightedFields(fieldsToDisplay, highlightedFieldsPrefixToExclude);
};

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/**
* The highlightedFieldsPrefixToExclude is an array of prefixes
* that should be disregarded in the Rule Exception.These prefixes
* are irrelevant to the exception and should be ignored,even if
* they were retrieved as Highlighted Fields from the "getEventFieldsToDisplay".
*/
export const highlightedFieldsPrefixToExclude = ['kibana.alert.rule', 'signal.rule', 'rule'];
export const getKibanaAlertIdField = (id: string) => `kibana.alert.${id}`;
export const EVENT_CATEGORY = 'event.category';
export const EVENT_CODE = 'event.code';
export const KIBANA_ALERT_RULE_TYPE = 'kibana.alert.rule.type';