mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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

**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:
parent
a13ac775b3
commit
9ebe5d5f53
11 changed files with 732 additions and 46 deletions
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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';
|
Loading…
Add table
Add a link
Reference in a new issue