mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Detection Engine][Rules] - Adds custom highlighted fields option (#163235)
## Summary Allows a user to define which fields to highlight in areas where we currently use "highlighted fields" feature.
This commit is contained in:
parent
847e0cbe72
commit
a772ab7fa8
78 changed files with 685 additions and 83 deletions
|
@ -20,6 +20,7 @@ const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE) => ({
|
|||
enabled: true,
|
||||
false_positives: ['false positive 1', 'false positive 2'],
|
||||
from: 'now-6m',
|
||||
investigation_fields: ['custom.field1', 'custom.field2'],
|
||||
immutable: false,
|
||||
name: 'Query with a rule id',
|
||||
query: 'user.name: root or user.name: admin',
|
||||
|
|
|
@ -55,6 +55,14 @@ export const RuleAuthorArray = t.array(t.string); // should be non-empty strings
|
|||
export type RuleFalsePositiveArray = t.TypeOf<typeof RuleFalsePositiveArray>;
|
||||
export const RuleFalsePositiveArray = t.array(t.string); // should be non-empty strings?
|
||||
|
||||
/**
|
||||
* User defined fields to display in areas such as alert details and exceptions auto-populate
|
||||
* Field added in PR - https://github.com/elastic/kibana/pull/163235
|
||||
* @example const investigationFields: RuleCustomHighlightedFieldArray = ['host.os.name']
|
||||
*/
|
||||
export type RuleCustomHighlightedFieldArray = t.TypeOf<typeof RuleCustomHighlightedFieldArray>;
|
||||
export const RuleCustomHighlightedFieldArray = t.array(NonEmptyString);
|
||||
|
||||
export type RuleReferenceArray = t.TypeOf<typeof RuleReferenceArray>;
|
||||
export const RuleReferenceArray = t.array(t.string); // should be non-empty strings?
|
||||
|
||||
|
|
|
@ -1289,6 +1289,36 @@ describe('rules schema', () => {
|
|||
expect(message.schema).toEqual({});
|
||||
expect(getPaths(left(message.errors))).toEqual(['invalid keys "data_view_id"']);
|
||||
});
|
||||
|
||||
test('You can optionally send in an array of investigation_fields', () => {
|
||||
const payload: RuleCreateProps = {
|
||||
...getCreateRulesSchemaMock(),
|
||||
investigation_fields: ['field1', 'field2'],
|
||||
};
|
||||
|
||||
const decoded = RuleCreateProps.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('You cannot send in an array of investigation_fields that are numbers', () => {
|
||||
const payload = {
|
||||
...getCreateRulesSchemaMock(),
|
||||
investigation_fields: [0, 1, 2],
|
||||
};
|
||||
|
||||
const decoded = RuleCreateProps.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "0" supplied to "investigation_fields"',
|
||||
'Invalid value "1" supplied to "investigation_fields"',
|
||||
'Invalid value "2" supplied to "investigation_fields"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('response', () => {
|
||||
|
|
|
@ -64,6 +64,7 @@ const getResponseBaseParams = (anchorDate: string = ANCHOR_DATE): SharedResponse
|
|||
timestamp_override: undefined,
|
||||
timestamp_override_fallback_disabled: undefined,
|
||||
namespace: undefined,
|
||||
investigation_fields: undefined,
|
||||
});
|
||||
|
||||
export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): QueryRule => ({
|
||||
|
@ -77,6 +78,7 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): QueryRule
|
|||
saved_id: undefined,
|
||||
response_actions: undefined,
|
||||
alert_suppression: undefined,
|
||||
investigation_fields: undefined,
|
||||
});
|
||||
|
||||
export const getSavedQuerySchemaMock = (anchorDate: string = ANCHOR_DATE): SavedQueryRule => ({
|
||||
|
|
|
@ -232,4 +232,65 @@ describe('Rule response schema', () => {
|
|||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('investigation_fields', () => {
|
||||
test('it should validate rule with empty array for "investigation_fields"', () => {
|
||||
const payload = getRulesSchemaMock();
|
||||
payload.investigation_fields = [];
|
||||
|
||||
const decoded = RuleResponse.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
const expected = { ...getRulesSchemaMock(), investigation_fields: [] };
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it should validate rule with "investigation_fields"', () => {
|
||||
const payload = getRulesSchemaMock();
|
||||
payload.investigation_fields = ['foo', 'bar'];
|
||||
|
||||
const decoded = RuleResponse.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
const expected = { ...getRulesSchemaMock(), investigation_fields: ['foo', 'bar'] };
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it should validate undefined for "investigation_fields"', () => {
|
||||
const payload: RuleResponse = {
|
||||
...getRulesSchemaMock(),
|
||||
investigation_fields: undefined,
|
||||
};
|
||||
|
||||
const decoded = RuleResponse.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
const expected = { ...getRulesSchemaMock(), investigation_fields: undefined };
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it should NOT validate a string for "investigation_fields"', () => {
|
||||
const payload: Omit<RuleResponse, 'investigation_fields'> & {
|
||||
investigation_fields: string;
|
||||
} = {
|
||||
...getRulesSchemaMock(),
|
||||
investigation_fields: 'foo',
|
||||
};
|
||||
|
||||
const decoded = RuleResponse.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "foo" supplied to "investigation_fields"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -53,6 +53,7 @@ import {
|
|||
RelatedIntegrationArray,
|
||||
RequiredFieldArray,
|
||||
RuleAuthorArray,
|
||||
RuleCustomHighlightedFieldArray,
|
||||
RuleDescription,
|
||||
RuleFalsePositiveArray,
|
||||
RuleFilterArray,
|
||||
|
@ -116,6 +117,7 @@ export const baseSchema = buildRuleSchemas({
|
|||
output_index: AlertsIndex,
|
||||
namespace: AlertsIndexNamespace,
|
||||
meta: RuleMetadata,
|
||||
investigation_fields: RuleCustomHighlightedFieldArray,
|
||||
// Throttle
|
||||
throttle: RuleActionThrottle,
|
||||
},
|
||||
|
|
|
@ -141,6 +141,45 @@ describe('AlertSummaryView', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
test('User specified investigation fields appear in summary rows', async () => {
|
||||
const mockData = mockAlertDetailsData.map((item) => {
|
||||
if (item.category === 'event' && item.field === 'event.category') {
|
||||
return {
|
||||
...item,
|
||||
values: ['network'],
|
||||
originalValue: ['network'],
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
const renderProps = {
|
||||
...props,
|
||||
investigationFields: ['custom.field'],
|
||||
data: [
|
||||
...mockData,
|
||||
{ category: 'custom', field: 'custom.field', values: ['blob'], originalValue: 'blob' },
|
||||
] as TimelineEventsDetailsItem[],
|
||||
};
|
||||
await act(async () => {
|
||||
const { getByText } = render(
|
||||
<TestProvidersComponent>
|
||||
<AlertSummaryView {...renderProps} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
[
|
||||
'custom.field',
|
||||
'host.name',
|
||||
'user.name',
|
||||
'destination.address',
|
||||
'source.address',
|
||||
'source.port',
|
||||
'process.name',
|
||||
].forEach((fieldId) => {
|
||||
expect(getByText(fieldId));
|
||||
});
|
||||
});
|
||||
});
|
||||
test('Network event renders the correct summary rows', async () => {
|
||||
const renderProps = {
|
||||
...props,
|
||||
|
|
|
@ -21,10 +21,30 @@ const AlertSummaryViewComponent: React.FC<{
|
|||
title: string;
|
||||
goToTable: () => void;
|
||||
isReadOnly?: boolean;
|
||||
}> = ({ browserFields, data, eventId, isDraggable, scopeId, title, goToTable, isReadOnly }) => {
|
||||
investigationFields?: string[];
|
||||
}> = ({
|
||||
browserFields,
|
||||
data,
|
||||
eventId,
|
||||
isDraggable,
|
||||
scopeId,
|
||||
title,
|
||||
goToTable,
|
||||
isReadOnly,
|
||||
investigationFields,
|
||||
}) => {
|
||||
const summaryRows = useMemo(
|
||||
() => getSummaryRows({ browserFields, data, eventId, isDraggable, scopeId, isReadOnly }),
|
||||
[browserFields, data, eventId, isDraggable, scopeId, isReadOnly]
|
||||
() =>
|
||||
getSummaryRows({
|
||||
browserFields,
|
||||
data,
|
||||
eventId,
|
||||
isDraggable,
|
||||
scopeId,
|
||||
isReadOnly,
|
||||
investigationFields,
|
||||
}),
|
||||
[browserFields, data, eventId, isDraggable, scopeId, isReadOnly, investigationFields]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -21,6 +21,8 @@ import styled from 'styled-components';
|
|||
import { isEmpty } from 'lodash';
|
||||
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers';
|
||||
import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback';
|
||||
import type { RawEventData } from '../../../../common/types/response_actions';
|
||||
import { useResponseActionsView } from './response_actions_view';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
|
||||
|
@ -169,6 +171,8 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
const goToTableTab = useCallback(() => setSelectedTabId(EventsViewType.tableView), []);
|
||||
|
||||
const eventFields = useMemo(() => getEnrichmentFields(data), [data]);
|
||||
const { ruleId } = useBasicDataFromDetailsData(data);
|
||||
const { rule: maybeRule } = useRuleWithFallback(ruleId);
|
||||
const existingEnrichments = useMemo(
|
||||
() =>
|
||||
isAlert
|
||||
|
@ -284,6 +288,7 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
isReadOnly,
|
||||
}}
|
||||
goToTable={goToTableTab}
|
||||
investigationFields={maybeRule?.investigation_fields ?? []}
|
||||
/>
|
||||
<EuiSpacer size="xl" />
|
||||
<Insights
|
||||
|
@ -337,6 +342,7 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
userRisk,
|
||||
allEnrichments,
|
||||
isEnrichmentsLoading,
|
||||
maybeRule,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -215,6 +215,15 @@ function getFieldsByRuleType(ruleType?: string): EventSummaryField[] {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fields to display based on custom rules and configuration
|
||||
* @param customs The list of custom-defined fields to display
|
||||
* @returns The list of custom-defined fields to display
|
||||
*/
|
||||
function getHighlightedFieldsOverride(customs: string[]): EventSummaryField[] {
|
||||
return customs.map((field) => ({ id: field }));
|
||||
}
|
||||
|
||||
/**
|
||||
This function is exported because it is used in the Exception Component to
|
||||
populate the conditions with the Highlighted Fields. Additionally, the new
|
||||
|
@ -229,12 +238,15 @@ export function getEventFieldsToDisplay({
|
|||
eventCategories,
|
||||
eventCode,
|
||||
eventRuleType,
|
||||
highlightedFieldsOverride,
|
||||
}: {
|
||||
eventCategories: EventCategories;
|
||||
eventCode?: string;
|
||||
eventRuleType?: string;
|
||||
highlightedFieldsOverride: string[];
|
||||
}): EventSummaryField[] {
|
||||
const fields = [
|
||||
...getHighlightedFieldsOverride(highlightedFieldsOverride),
|
||||
...alwaysDisplayedFields,
|
||||
...getFieldsByCategory(eventCategories),
|
||||
...getFieldsByEventCode(eventCode, eventCategories),
|
||||
|
@ -281,11 +293,13 @@ export const getSummaryRows = ({
|
|||
eventId,
|
||||
isDraggable = false,
|
||||
isReadOnly = false,
|
||||
investigationFields,
|
||||
}: {
|
||||
data: TimelineEventsDetailsItem[];
|
||||
browserFields: BrowserFields;
|
||||
scopeId: string;
|
||||
eventId: string;
|
||||
investigationFields?: string[];
|
||||
isDraggable?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
}) => {
|
||||
|
@ -306,6 +320,7 @@ export const getSummaryRows = ({
|
|||
eventCategories,
|
||||
eventCode,
|
||||
eventRuleType,
|
||||
highlightedFieldsOverride: investigationFields ?? [],
|
||||
});
|
||||
|
||||
return data != null
|
||||
|
|
|
@ -555,6 +555,7 @@ describe('helpers', () => {
|
|||
severity_mapping: [],
|
||||
tags: ['tag1', 'tag2'],
|
||||
threat: getThreatMock(),
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
@ -635,6 +636,7 @@ describe('helpers', () => {
|
|||
severity_mapping: [],
|
||||
tags: ['tag1', 'tag2'],
|
||||
threat: getThreatMock(),
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
@ -659,6 +661,7 @@ describe('helpers', () => {
|
|||
severity_mapping: [],
|
||||
tags: ['tag1', 'tag2'],
|
||||
threat: getThreatMock(),
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
@ -702,6 +705,7 @@ describe('helpers', () => {
|
|||
severity_mapping: [],
|
||||
tags: ['tag1', 'tag2'],
|
||||
threat: getThreatMock(),
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
@ -754,6 +758,7 @@ describe('helpers', () => {
|
|||
],
|
||||
},
|
||||
],
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
@ -782,6 +787,95 @@ describe('helpers', () => {
|
|||
threat: getThreatMock(),
|
||||
timestamp_override: 'event.ingest',
|
||||
timestamp_override_fallback_disabled: true,
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns formatted object if investigation_fields is empty array', () => {
|
||||
const mockStepData: AboutStepRule = {
|
||||
...mockData,
|
||||
investigationFields: [],
|
||||
};
|
||||
const result = formatAboutStepData(mockStepData);
|
||||
const expected: AboutStepRuleJson = {
|
||||
author: ['Elastic'],
|
||||
description: '24/7',
|
||||
false_positives: ['test'],
|
||||
license: 'Elastic License',
|
||||
name: 'Query with rule-id',
|
||||
note: '# this is some markdown documentation',
|
||||
references: ['www.test.co'],
|
||||
risk_score: 21,
|
||||
risk_score_mapping: [],
|
||||
severity: 'low',
|
||||
severity_mapping: [],
|
||||
tags: ['tag1', 'tag2'],
|
||||
rule_name_override: undefined,
|
||||
threat_indicator_path: undefined,
|
||||
timestamp_override: undefined,
|
||||
timestamp_override_fallback_disabled: undefined,
|
||||
threat: getThreatMock(),
|
||||
investigation_fields: [],
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns formatted object with investigation_fields', () => {
|
||||
const mockStepData: AboutStepRule = {
|
||||
...mockData,
|
||||
investigationFields: ['foo', 'bar'],
|
||||
};
|
||||
const result = formatAboutStepData(mockStepData);
|
||||
const expected: AboutStepRuleJson = {
|
||||
author: ['Elastic'],
|
||||
description: '24/7',
|
||||
false_positives: ['test'],
|
||||
license: 'Elastic License',
|
||||
name: 'Query with rule-id',
|
||||
note: '# this is some markdown documentation',
|
||||
references: ['www.test.co'],
|
||||
risk_score: 21,
|
||||
risk_score_mapping: [],
|
||||
severity: 'low',
|
||||
severity_mapping: [],
|
||||
tags: ['tag1', 'tag2'],
|
||||
threat: getThreatMock(),
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
threat_indicator_path: undefined,
|
||||
timestamp_override: undefined,
|
||||
timestamp_override_fallback_disabled: undefined,
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns formatted object if investigation_fields includes empty string', () => {
|
||||
const mockStepData: AboutStepRule = {
|
||||
...mockData,
|
||||
investigationFields: [' '],
|
||||
};
|
||||
const result = formatAboutStepData(mockStepData);
|
||||
const expected: AboutStepRuleJson = {
|
||||
author: ['Elastic'],
|
||||
description: '24/7',
|
||||
false_positives: ['test'],
|
||||
license: 'Elastic License',
|
||||
name: 'Query with rule-id',
|
||||
note: '# this is some markdown documentation',
|
||||
references: ['www.test.co'],
|
||||
risk_score: 21,
|
||||
risk_score_mapping: [],
|
||||
severity: 'low',
|
||||
severity_mapping: [],
|
||||
tags: ['tag1', 'tag2'],
|
||||
threat: getThreatMock(),
|
||||
investigation_fields: [],
|
||||
threat_indicator_path: undefined,
|
||||
timestamp_override: undefined,
|
||||
timestamp_override_fallback_disabled: undefined,
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
|
|
@ -485,6 +485,7 @@ export const formatAboutStepData = (
|
|||
const {
|
||||
author,
|
||||
falsePositives,
|
||||
investigationFields,
|
||||
references,
|
||||
riskScore,
|
||||
severity,
|
||||
|
@ -524,6 +525,7 @@ export const formatAboutStepData = (
|
|||
: {}),
|
||||
false_positives: falsePositives.filter((item) => !isEmpty(item)),
|
||||
references: references.filter((item) => !isEmpty(item)),
|
||||
investigation_fields: investigationFields.filter((item) => !isEmpty(item.trim())),
|
||||
risk_score: riskScore.value,
|
||||
risk_score_mapping: riskScore.isMappingChecked
|
||||
? riskScore.mapping.filter((m) => m.field != null && m.field !== '')
|
||||
|
|
|
@ -662,6 +662,42 @@ describe('When the add exception modal is opened', () => {
|
|||
expect(getByTestId('entryType')).toHaveTextContent('match');
|
||||
expect(getByTestId('entryValue')).toHaveTextContent('test/path');
|
||||
});
|
||||
|
||||
it('should include rule defined custom highlighted fields', () => {
|
||||
const wrapper = render(
|
||||
(() => (
|
||||
<TestProviders>
|
||||
<AddExceptionFlyout
|
||||
rules={[
|
||||
{
|
||||
...getRulesSchemaMock(),
|
||||
investigation_fields: ['foo.bar'],
|
||||
exceptions_list: [],
|
||||
} as Rule,
|
||||
]}
|
||||
isBulkAction={false}
|
||||
alertData={{ ...alertDataMock, foo: { bar: 'blob' } } as AlertData}
|
||||
isAlertDataLoading={false}
|
||||
alertStatus="open"
|
||||
isEndpointItem={false}
|
||||
showAlertCloseOptions
|
||||
onCancel={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
))()
|
||||
);
|
||||
const { getByTestId, getAllByTestId } = wrapper;
|
||||
expect(getByTestId('alertExceptionBuilder')).toBeInTheDocument();
|
||||
expect(getAllByTestId('entryField')[0]).toHaveTextContent('foo.bar');
|
||||
expect(getAllByTestId('entryOperator')[0]).toHaveTextContent('included');
|
||||
expect(getAllByTestId('entryType')[0]).toHaveTextContent('match');
|
||||
expect(getAllByTestId('entryValue')[0]).toHaveTextContent('blob');
|
||||
expect(getAllByTestId('entryField')[1]).toHaveTextContent('file.path');
|
||||
expect(getAllByTestId('entryOperator')[1]).toHaveTextContent('included');
|
||||
expect(getAllByTestId('entryType')[1]).toHaveTextContent('match');
|
||||
expect(getAllByTestId('entryValue')[1]).toHaveTextContent('test/path');
|
||||
});
|
||||
});
|
||||
describe('bulk closeable alert data is passed in', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
|
|
|
@ -346,6 +346,9 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
|
|||
const populatedException = getPrepopulatedRuleExceptionWithHighlightFields({
|
||||
alertData,
|
||||
exceptionItemName,
|
||||
// With "rule_default" type, there is only ever one rule associated.
|
||||
// That is why it's ok to pull just the first item from rules array here.
|
||||
ruleCustomHighlightedFields: rules?.[0]?.investigation_fields ?? [],
|
||||
});
|
||||
if (populatedException) {
|
||||
setComment(i18n.ADD_RULE_EXCEPTION_FROM_ALERT_COMMENT(alertData._id));
|
||||
|
@ -354,7 +357,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
|
|||
}
|
||||
}
|
||||
}
|
||||
}, [listType, exceptionItemName, alertData, setInitialExceptionItems, setComment]);
|
||||
}, [listType, exceptionItemName, alertData, rules, setInitialExceptionItems, setComment]);
|
||||
|
||||
const osTypesSelection = useMemo((): OsTypeArray => {
|
||||
return hasAlertData ? retrieveAlertOsTypes(alertData) : selectedOs ? [...selectedOs] : [];
|
||||
|
|
|
@ -1560,6 +1560,27 @@ describe('Exception helpers', () => {
|
|||
},
|
||||
{ field: 'process.name', operator: 'included', type: 'match', value: 'malware writer' },
|
||||
];
|
||||
const expectedExceptionEntriesWithCustomHighlightedFields = [
|
||||
{
|
||||
field: 'event.type',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: 'creation',
|
||||
},
|
||||
{
|
||||
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,
|
||||
|
@ -1739,12 +1760,12 @@ describe('Exception helpers', () => {
|
|||
},
|
||||
];
|
||||
it('should return the highlighted fields correctly when eventCode, eventCategory and RuleType are in the alertData', () => {
|
||||
const res = getAlertHighlightedFields(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);
|
||||
const res = getAlertHighlightedFields(alertDataWithoutEventCode, []);
|
||||
expect(res).toEqual([
|
||||
...baseGeneratedAlertHighlightedFields,
|
||||
{
|
||||
|
@ -1763,7 +1784,7 @@ describe('Exception helpers', () => {
|
|||
});
|
||||
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);
|
||||
const res = getAlertHighlightedFields(alertDataWithoutEventCategory, []);
|
||||
expect(res).toEqual([
|
||||
...baseGeneratedAlertHighlightedFields,
|
||||
{
|
||||
|
@ -1775,7 +1796,7 @@ describe('Exception helpers', () => {
|
|||
});
|
||||
it('should return the process highlighted fields correctly when eventCategory is an array', () => {
|
||||
const alertDataEventCategoryProcessArray = { ...alertData, 'event.category': ['process'] };
|
||||
const res = getAlertHighlightedFields(alertDataEventCategoryProcessArray);
|
||||
const res = getAlertHighlightedFields(alertDataEventCategoryProcessArray, []);
|
||||
expect(res).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
{ id: 'file.name' },
|
||||
|
@ -1793,20 +1814,20 @@ describe('Exception helpers', () => {
|
|||
});
|
||||
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);
|
||||
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);
|
||||
const res = getAlertHighlightedFields(alertData, []);
|
||||
expect(res).toEqual(allHighlightFields);
|
||||
});
|
||||
it('should exclude the "agent.id" from highlighted fields when agent.type is not "endpoint"', () => {
|
||||
jest.mock('./highlighted_fields_config', () => ({ highlightedFieldsPrefixToExclude: [] }));
|
||||
|
||||
const alertDataWithoutAgentType = { ...alertData, agent: { ...alertData.agent, type: '' } };
|
||||
const res = getAlertHighlightedFields(alertDataWithoutAgentType);
|
||||
const res = getAlertHighlightedFields(alertDataWithoutAgentType, []);
|
||||
|
||||
expect(res).toEqual(allHighlightFields.filter((field) => field.id !== AGENT_ID));
|
||||
});
|
||||
|
@ -1814,10 +1835,14 @@ describe('Exception helpers', () => {
|
|||
jest.mock('./highlighted_fields_config', () => ({ highlightedFieldsPrefixToExclude: [] }));
|
||||
|
||||
const alertDataWithoutRuleUUID = { ...alertData, 'kibana.alert.rule.uuid': '' };
|
||||
const res = getAlertHighlightedFields(alertDataWithoutRuleUUID);
|
||||
const res = getAlertHighlightedFields(alertDataWithoutRuleUUID, []);
|
||||
|
||||
expect(res).toEqual(allHighlightFields.filter((field) => field.id !== AGENT_ID));
|
||||
});
|
||||
it('should include custom highlighted fields', () => {
|
||||
const res = getAlertHighlightedFields(alertData, ['event.type']);
|
||||
expect(res).toEqual([{ id: 'event.type' }, ...allHighlightFields]);
|
||||
});
|
||||
});
|
||||
describe('getPrepopulatedRuleExceptionWithHighlightFields', () => {
|
||||
it('should not create any exception and return null if there are no highlighted fields', () => {
|
||||
|
@ -1826,6 +1851,7 @@ describe('Exception helpers', () => {
|
|||
const res = getPrepopulatedRuleExceptionWithHighlightFields({
|
||||
alertData: defaultAlertData,
|
||||
exceptionItemName: '',
|
||||
ruleCustomHighlightedFields: [],
|
||||
});
|
||||
expect(res).toBe(null);
|
||||
});
|
||||
|
@ -1835,6 +1861,7 @@ describe('Exception helpers', () => {
|
|||
const res = getPrepopulatedRuleExceptionWithHighlightFields({
|
||||
alertData: defaultAlertData,
|
||||
exceptionItemName: '',
|
||||
ruleCustomHighlightedFields: [],
|
||||
});
|
||||
expect(res).toBe(null);
|
||||
});
|
||||
|
@ -1842,6 +1869,7 @@ describe('Exception helpers', () => {
|
|||
const exception = getPrepopulatedRuleExceptionWithHighlightFields({
|
||||
alertData,
|
||||
exceptionItemName: name,
|
||||
ruleCustomHighlightedFields: [],
|
||||
});
|
||||
|
||||
expect(exception?.entries).toEqual(
|
||||
|
@ -1849,6 +1877,21 @@ describe('Exception helpers', () => {
|
|||
);
|
||||
expect(exception?.name).toEqual(name);
|
||||
});
|
||||
it('should create a new exception and populate its entries with the custom highlighted fields', () => {
|
||||
const exception = getPrepopulatedRuleExceptionWithHighlightFields({
|
||||
alertData,
|
||||
exceptionItemName: name,
|
||||
ruleCustomHighlightedFields: ['event.type'],
|
||||
});
|
||||
|
||||
expect(exception?.entries).toEqual(
|
||||
expectedExceptionEntriesWithCustomHighlightedFields.map((entry) => ({
|
||||
...entry,
|
||||
id: '123',
|
||||
}))
|
||||
);
|
||||
expect(exception?.name).toEqual(name);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -908,11 +908,13 @@ export const buildExceptionEntriesFromAlertFields = ({
|
|||
export const getPrepopulatedRuleExceptionWithHighlightFields = ({
|
||||
alertData,
|
||||
exceptionItemName,
|
||||
ruleCustomHighlightedFields,
|
||||
}: {
|
||||
alertData: AlertData;
|
||||
exceptionItemName: string;
|
||||
ruleCustomHighlightedFields: string[];
|
||||
}): ExceptionsBuilderExceptionItem | null => {
|
||||
const highlightedFields = getAlertHighlightedFields(alertData);
|
||||
const highlightedFields = getAlertHighlightedFields(alertData, ruleCustomHighlightedFields);
|
||||
if (!highlightedFields.length) return null;
|
||||
|
||||
const exceptionEntries = buildExceptionEntriesFromAlertFields({ highlightedFields, alertData });
|
||||
|
@ -951,11 +953,13 @@ export const filterHighlightedFields = (
|
|||
* * Alert field ids filters
|
||||
* @param alertData The Alert data object
|
||||
*/
|
||||
export const getAlertHighlightedFields = (alertData: AlertData): EventSummaryField[] => {
|
||||
export const getAlertHighlightedFields = (
|
||||
alertData: AlertData,
|
||||
ruleCustomHighlightedFields: string[]
|
||||
): EventSummaryField[] => {
|
||||
const eventCategory = get(alertData, EVENT_CATEGORY);
|
||||
const eventCode = get(alertData, EVENT_CODE);
|
||||
const eventRuleType = get(alertData, KIBANA_ALERT_RULE_TYPE);
|
||||
|
||||
const eventCategories = {
|
||||
primaryEventCategory: Array.isArray(eventCategory) ? eventCategory[0] : eventCategory,
|
||||
allEventCategories: [eventCategory],
|
||||
|
@ -965,6 +969,7 @@ export const getAlertHighlightedFields = (alertData: AlertData): EventSummaryFie
|
|||
eventCategories,
|
||||
eventCode,
|
||||
eventRuleType,
|
||||
highlightedFieldsOverride: ruleCustomHighlightedFields,
|
||||
});
|
||||
return filterHighlightedFields(fieldsToDisplay, highlightedFieldsPrefixToExclude, alertData);
|
||||
};
|
||||
|
|
|
@ -73,6 +73,7 @@ import {
|
|||
TimestampField,
|
||||
TimestampOverride,
|
||||
TimestampOverrideFallbackDisabled,
|
||||
RuleCustomHighlightedFieldArray,
|
||||
} from '../../../../common/api/detection_engine/model/rule_schema';
|
||||
|
||||
import type {
|
||||
|
@ -201,6 +202,7 @@ export const RuleSchema = t.intersection([
|
|||
version: RuleVersion,
|
||||
execution_summary: RuleExecutionSummary,
|
||||
alert_suppression: AlertSuppression,
|
||||
investigation_fields: RuleCustomHighlightedFieldArray,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
@ -194,6 +194,7 @@ export const mockAboutStepRule = (): AboutStepRule => ({
|
|||
tags: ['tag1', 'tag2'],
|
||||
threat: getThreatMock(),
|
||||
note: '# this is some markdown documentation',
|
||||
investigationFields: ['foo', 'bar'],
|
||||
});
|
||||
|
||||
export const mockActionsStepRule = (enabled = false): ActionsStepRule => ({
|
||||
|
|
|
@ -15,6 +15,7 @@ import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
|||
import { get } from 'lodash/fp';
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import { TableId } from '@kbn/securitysolution-data-table';
|
||||
import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback';
|
||||
import { DEFAULT_ACTION_BUTTON_WIDTH } from '../../../../common/components/header_actions';
|
||||
import { isActiveTimeline } from '../../../../helpers';
|
||||
import { useOsqueryContextActionItem } from '../../osquery/use_osquery_context_action_item';
|
||||
|
@ -384,6 +385,7 @@ export const AddExceptionFlyoutWrapper: React.FC<AddExceptionFlyoutWrapperProps>
|
|||
alertStatus,
|
||||
}) => {
|
||||
const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex();
|
||||
const { rule: maybeRule, loading: isRuleLoading } = useRuleWithFallback(ruleId);
|
||||
|
||||
const { loading: isLoadingAlertData, data } = useQueryAlerts<EcsHit, {}>({
|
||||
query: buildGetAlertByIdQuery(eventId),
|
||||
|
@ -429,32 +431,13 @@ export const AddExceptionFlyoutWrapper: React.FC<AddExceptionFlyoutWrapperProps>
|
|||
return ruleDataViewId;
|
||||
}, [enrichedAlert, ruleDataViewId]);
|
||||
|
||||
// TODO: Do we want to notify user when they are working off of an older version of a rule
|
||||
// if they select to add an exception from an alert referencing an older rule version?
|
||||
const memoRule = useMemo(() => {
|
||||
if (enrichedAlert != null && enrichedAlert['kibana.alert.rule.parameters'] != null) {
|
||||
return [
|
||||
{
|
||||
...enrichedAlert['kibana.alert.rule.parameters'],
|
||||
id: ruleId,
|
||||
rule_id: ruleRuleId,
|
||||
name: ruleName,
|
||||
index: memoRuleIndices,
|
||||
data_view_id: memoDataViewId,
|
||||
},
|
||||
] as Rule[];
|
||||
if (maybeRule) {
|
||||
return [maybeRule];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: ruleId,
|
||||
rule_id: ruleRuleId,
|
||||
name: ruleName,
|
||||
index: memoRuleIndices,
|
||||
data_view_id: memoDataViewId,
|
||||
},
|
||||
] as Rule[];
|
||||
}, [enrichedAlert, memoDataViewId, memoRuleIndices, ruleId, ruleName, ruleRuleId]);
|
||||
return null;
|
||||
}, [maybeRule]);
|
||||
|
||||
const isLoading =
|
||||
(isLoadingAlertData && isSignalIndexLoading) ||
|
||||
|
@ -466,7 +449,7 @@ export const AddExceptionFlyoutWrapper: React.FC<AddExceptionFlyoutWrapperProps>
|
|||
rules={memoRule}
|
||||
isEndpointItem={exceptionListType === ExceptionListTypeEnum.ENDPOINT}
|
||||
alertData={enrichedAlert}
|
||||
isAlertDataLoading={isLoading}
|
||||
isAlertDataLoading={isLoading || isRuleLoading}
|
||||
alertStatus={alertStatus}
|
||||
isBulkAction={false}
|
||||
showAlertCloseOptions
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
buildUrlsDescription,
|
||||
buildNoteDescription,
|
||||
buildRuleTypeDescription,
|
||||
buildHighlightedFieldsOverrideDescription,
|
||||
} from './helpers';
|
||||
import type { ListItems } from './types';
|
||||
|
||||
|
@ -508,4 +509,28 @@ describe('helpers', () => {
|
|||
expect(result.description).toEqual('Indicator Match');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildHighlightedFieldsOverrideDescription', () => {
|
||||
test('returns ListItem with passed in label and custom highlighted fields', () => {
|
||||
const result: ListItems[] = buildHighlightedFieldsOverrideDescription('Test label', [
|
||||
'foo',
|
||||
'bar',
|
||||
]);
|
||||
const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement);
|
||||
const element = wrapper.find(
|
||||
'[data-test-subj="customHighlightedFieldsStringArrayDescriptionBadgeItem"]'
|
||||
);
|
||||
|
||||
expect(result[0].title).toEqual('Test label');
|
||||
expect(element.exists()).toBeTruthy();
|
||||
expect(element.at(0).text()).toEqual('foo');
|
||||
expect(element.at(1).text()).toEqual('bar');
|
||||
});
|
||||
|
||||
test('returns empty array if passed in note is empty string', () => {
|
||||
const result: ListItems[] = buildHighlightedFieldsOverrideDescription('Test label', []);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,15 +27,13 @@ import { FieldIcon } from '@kbn/react-field';
|
|||
|
||||
import type { ThreatMapping, Type } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import { FilterBadgeGroup } from '@kbn/unified-search-plugin/public';
|
||||
import type { RequiredFieldArray } from '../../../../../common/api/detection_engine/model/rule_schema/common_attributes';
|
||||
import { MATCHES, AND, OR } from '../../../../common/components/threat_match/translations';
|
||||
import type { EqlOptionsSelected } from '../../../../../common/search_strategy';
|
||||
import { assertUnreachable } from '../../../../../common/utility_types';
|
||||
import * as i18nSeverity from '../severity_mapping/translations';
|
||||
import * as i18nRiskScore from '../risk_score_mapping/translations';
|
||||
import type {
|
||||
RequiredFieldArray,
|
||||
Threshold,
|
||||
} from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import type { Threshold } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import type { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './types';
|
||||
|
@ -201,6 +199,38 @@ export const buildUnorderedListArrayDescription = (
|
|||
return [];
|
||||
};
|
||||
|
||||
export const buildHighlightedFieldsOverrideDescription = (
|
||||
label: string,
|
||||
values: string[]
|
||||
): ListItems[] => {
|
||||
if (isEmpty(values)) {
|
||||
return [];
|
||||
}
|
||||
const description = (
|
||||
<EuiFlexGroup responsive={false} gutterSize="xs" wrap>
|
||||
{values.map((val: string) =>
|
||||
isEmpty(val) ? null : (
|
||||
<EuiFlexItem grow={false} key={`${label}-${val}`}>
|
||||
<EuiBadgeWrap
|
||||
data-test-subj="customHighlightedFieldsStringArrayDescriptionBadgeItem"
|
||||
color="hollow"
|
||||
>
|
||||
{val}
|
||||
</EuiBadgeWrap>
|
||||
</EuiFlexItem>
|
||||
)
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
title: label,
|
||||
description,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const buildStringArrayDescription = (
|
||||
label: string,
|
||||
field: string,
|
||||
|
|
|
@ -262,7 +262,7 @@ describe('description_step', () => {
|
|||
mockLicenseService
|
||||
);
|
||||
|
||||
expect(result.length).toEqual(11);
|
||||
expect(result.length).toEqual(12);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -14,11 +14,11 @@ import type { ThreatMapping, Threats, Type } from '@kbn/securitysolution-io-ts-a
|
|||
import type { DataViewBase, Filter } from '@kbn/es-query';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
import { FilterManager } from '@kbn/data-plugin/public';
|
||||
import { buildRelatedIntegrationsDescription } from '../related_integrations/integrations_description';
|
||||
import type {
|
||||
RelatedIntegrationArray,
|
||||
RequiredFieldArray,
|
||||
} from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
} from '../../../../../common/api/detection_engine/model/rule_schema/common_attributes';
|
||||
import { buildRelatedIntegrationsDescription } from '../related_integrations/integrations_description';
|
||||
import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations';
|
||||
import type { EqlOptionsSelected } from '../../../../../common/search_strategy';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
@ -47,6 +47,7 @@ import {
|
|||
buildAlertSuppressionDescription,
|
||||
buildAlertSuppressionWindowDescription,
|
||||
buildAlertSuppressionMissingFieldsDescription,
|
||||
buildHighlightedFieldsOverrideDescription,
|
||||
} from './helpers';
|
||||
import * as i18n from './translations';
|
||||
import { buildMlJobsDescription } from './build_ml_jobs_description';
|
||||
|
@ -261,6 +262,9 @@ export const getDescriptionItem = (
|
|||
} else if (field === 'falsePositives') {
|
||||
const values: string[] = get(field, data);
|
||||
return buildUnorderedListArrayDescription(label, field, values);
|
||||
} else if (field === 'investigationFields') {
|
||||
const values: string[] = get(field, data);
|
||||
return buildHighlightedFieldsOverrideDescription(label, values);
|
||||
} else if (field === 'riskScore') {
|
||||
const values: AboutStepRiskScore = get(field, data);
|
||||
return buildRiskScoreDescription(values);
|
||||
|
|
|
@ -11,40 +11,44 @@ import { EuiToolTip } from '@elastic/eui';
|
|||
import type { DataViewFieldBase } from '@kbn/es-query';
|
||||
import type { FieldHook } from '../../../../shared_imports';
|
||||
import { Field } from '../../../../shared_imports';
|
||||
import { GROUP_BY_FIELD_PLACEHOLDER, GROUP_BY_FIELD_LICENSE_WARNING } from './translations';
|
||||
import { FIELD_PLACEHOLDER } from './translations';
|
||||
|
||||
interface GroupByFieldsProps {
|
||||
interface MultiSelectAutocompleteProps {
|
||||
browserFields: DataViewFieldBase[];
|
||||
isDisabled: boolean;
|
||||
field: FieldHook;
|
||||
fullWidth?: boolean;
|
||||
disabledText?: string;
|
||||
}
|
||||
|
||||
const FIELD_COMBO_BOX_WIDTH = 410;
|
||||
|
||||
const fieldDescribedByIds = 'detectionEngineStepDefineRuleGroupByField';
|
||||
const fieldDescribedByIds = 'detectionEngineMultiSelectAutocompleteField';
|
||||
|
||||
export const GroupByComponent: React.FC<GroupByFieldsProps> = ({
|
||||
export const MultiSelectAutocompleteComponent: React.FC<MultiSelectAutocompleteProps> = ({
|
||||
browserFields,
|
||||
disabledText,
|
||||
isDisabled,
|
||||
field,
|
||||
}: GroupByFieldsProps) => {
|
||||
fullWidth = false,
|
||||
}: MultiSelectAutocompleteProps) => {
|
||||
const fieldEuiFieldProps = useMemo(
|
||||
() => ({
|
||||
fullWidth: true,
|
||||
noSuggestions: false,
|
||||
options: browserFields.map((browserField) => ({ label: browserField.name })),
|
||||
placeholder: GROUP_BY_FIELD_PLACEHOLDER,
|
||||
placeholder: FIELD_PLACEHOLDER,
|
||||
onCreateOption: undefined,
|
||||
style: { width: `${FIELD_COMBO_BOX_WIDTH}px` },
|
||||
...(fullWidth ? {} : { style: { width: `${FIELD_COMBO_BOX_WIDTH}px` } }),
|
||||
isDisabled,
|
||||
}),
|
||||
[browserFields, isDisabled]
|
||||
[browserFields, isDisabled, fullWidth]
|
||||
);
|
||||
const fieldComponent = (
|
||||
<Field field={field} idAria={fieldDescribedByIds} euiFieldProps={fieldEuiFieldProps} />
|
||||
);
|
||||
return isDisabled ? (
|
||||
<EuiToolTip position="right" content={GROUP_BY_FIELD_LICENSE_WARNING}>
|
||||
<EuiToolTip position="right" content={disabledText}>
|
||||
{fieldComponent}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
|
@ -52,4 +56,4 @@ export const GroupByComponent: React.FC<GroupByFieldsProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const GroupByFields = React.memo(GroupByComponent);
|
||||
export const MultiSelectFieldsAutocomplete = React.memo(MultiSelectAutocompleteComponent);
|
|
@ -7,16 +7,9 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const GROUP_BY_FIELD_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.placeholderText',
|
||||
export const FIELD_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.multiSelectFields.placeholderText',
|
||||
{
|
||||
defaultMessage: 'Select a field',
|
||||
}
|
||||
);
|
||||
|
||||
export const GROUP_BY_FIELD_LICENSE_WARNING = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.licenseWarning',
|
||||
{
|
||||
defaultMessage: 'Alert suppression is enabled with Platinum license or above',
|
||||
}
|
||||
);
|
|
@ -26,6 +26,7 @@ export const stepAboutDefaultValue: AboutStepRule = {
|
|||
riskScore: { value: 21, mapping: [], isMappingChecked: false },
|
||||
references: [''],
|
||||
falsePositives: [''],
|
||||
investigationFields: [],
|
||||
license: '',
|
||||
ruleNameOverride: '',
|
||||
tags: [],
|
||||
|
|
|
@ -274,6 +274,7 @@ describe('StepAboutRuleComponent', () => {
|
|||
technique: [],
|
||||
},
|
||||
],
|
||||
investigationFields: [],
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
|
@ -333,6 +334,7 @@ describe('StepAboutRuleComponent', () => {
|
|||
technique: [],
|
||||
},
|
||||
],
|
||||
investigationFields: [],
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
|
|
|
@ -33,6 +33,7 @@ import { useFetchIndex } from '../../../../common/containers/source';
|
|||
import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { useRuleIndices } from '../../../../detection_engine/rule_management/logic/use_rule_indices';
|
||||
import { MultiSelectFieldsAutocomplete } from '../multi_select_fields';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
|
@ -237,6 +238,16 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
|
|||
}}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<UseField
|
||||
path="investigationFields"
|
||||
component={MultiSelectFieldsAutocomplete}
|
||||
componentProps={{
|
||||
browserFields: indexPattern.fields,
|
||||
isDisabled: isLoading || indexPatternLoading,
|
||||
fullWidth: true,
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<UseField
|
||||
path="note"
|
||||
component={MarkdownEditorForm}
|
||||
|
|
|
@ -160,6 +160,16 @@ export const schema: FormSchema<AboutStepRule> = {
|
|||
),
|
||||
labelAppend: OptionalFieldLabel,
|
||||
},
|
||||
investigationFields: {
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
label: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldCustomHighlightedFieldsLabel',
|
||||
{
|
||||
defaultMessage: 'Custom highlighted fields',
|
||||
}
|
||||
),
|
||||
labelAppend: OptionalFieldLabel,
|
||||
},
|
||||
license: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.translate(
|
||||
|
|
|
@ -28,6 +28,13 @@ export const ADD_FALSE_POSITIVE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ADD_CUSTOM_HIGHLIGHTED_FIELD = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addCustomHighlightedFieldDescription',
|
||||
{
|
||||
defaultMessage: 'Add a custom highlighted field',
|
||||
}
|
||||
);
|
||||
|
||||
export const GLOBAL_ENDPOINT_EXCEPTION_LIST = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.endpointExceptionListLabel',
|
||||
{
|
||||
|
|
|
@ -75,7 +75,7 @@ import { NewTermsFields } from '../new_terms_fields';
|
|||
import { ScheduleItem } from '../schedule_item_form';
|
||||
import { DocLink } from '../../../../common/components/links_to_docs/doc_link';
|
||||
import { defaultCustomQuery } from '../../../pages/detection_engine/rules/utils';
|
||||
import { GroupByFields } from '../group_by_fields';
|
||||
import { MultiSelectFieldsAutocomplete } from '../multi_select_fields';
|
||||
import { useLicense } from '../../../../common/hooks/use_license';
|
||||
import {
|
||||
minimumLicenseForSuppression,
|
||||
|
@ -752,9 +752,10 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
>
|
||||
<UseField
|
||||
path="groupByFields"
|
||||
component={GroupByFields}
|
||||
component={MultiSelectFieldsAutocomplete}
|
||||
componentProps={{
|
||||
browserFields: termsAggregationFields,
|
||||
disabledText: i18n.GROUP_BY_FIELD_LICENSE_WARNING,
|
||||
isDisabled:
|
||||
!license.isAtLeast(minimumLicenseForSuppression) && groupByFields?.length === 0,
|
||||
}}
|
||||
|
|
|
@ -171,3 +171,10 @@ export const ALERT_SUPPRESSION_MISSING_FIELDS_DO_NOT_SUPPRESS_OPTION = i18n.tran
|
|||
defaultMessage: 'Do not suppress alerts for events with missing fields',
|
||||
}
|
||||
);
|
||||
|
||||
export const GROUP_BY_FIELD_LICENSE_WARNING = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.licenseWarning',
|
||||
{
|
||||
defaultMessage: 'Alert suppression is enabled with Platinum license or above',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -144,6 +144,7 @@ describe('rule helpers', () => {
|
|||
threat: getThreatMock(),
|
||||
timestampOverride: 'event.ingested',
|
||||
timestampOverrideFallbackDisabled: false,
|
||||
investigationFields: [],
|
||||
};
|
||||
const scheduleRuleStepData = { from: '0s', interval: '5m' };
|
||||
const ruleActionsStepData = {
|
||||
|
@ -181,6 +182,14 @@ describe('rule helpers', () => {
|
|||
|
||||
expect(result.note).toEqual('');
|
||||
});
|
||||
|
||||
test('returns customHighlightedField as empty array if property does not exist on rule', () => {
|
||||
const mockedRule = mockRuleWithEverything('test-id');
|
||||
delete mockedRule.investigation_fields;
|
||||
const result: AboutStepRule = getAboutStepsData(mockedRule, false);
|
||||
|
||||
expect(result.investigationFields).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('determineDetailsValue', () => {
|
||||
|
|
|
@ -200,6 +200,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu
|
|||
severity,
|
||||
false_positives: falsePositives,
|
||||
risk_score: riskScore,
|
||||
investigation_fields: investigationFields,
|
||||
tags,
|
||||
threat,
|
||||
threat_indicator_path: threatIndicatorPath,
|
||||
|
@ -230,6 +231,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu
|
|||
isMappingChecked: riskScoreMapping.length > 0,
|
||||
},
|
||||
falsePositives,
|
||||
investigationFields: investigationFields ?? [],
|
||||
threat: threat as Threats,
|
||||
threatIndicatorPath,
|
||||
};
|
||||
|
@ -343,6 +345,7 @@ const commonRuleParamsKeys = [
|
|||
'name',
|
||||
'description',
|
||||
'false_positives',
|
||||
'investigation_fields',
|
||||
'rule_id',
|
||||
'max_signals',
|
||||
'risk_score',
|
||||
|
|
|
@ -89,6 +89,7 @@ export interface AboutStepRule {
|
|||
riskScore: AboutStepRiskScore;
|
||||
references: string[];
|
||||
falsePositives: string[];
|
||||
investigationFields: string[];
|
||||
license: string;
|
||||
ruleNameOverride: string;
|
||||
tags: string[];
|
||||
|
@ -238,6 +239,7 @@ export interface AboutStepRuleJson {
|
|||
timestamp_override?: TimestampOverride;
|
||||
timestamp_override_fallback_disabled?: boolean;
|
||||
note?: string;
|
||||
investigation_fields?: string[];
|
||||
}
|
||||
|
||||
export interface ScheduleStepRuleJson {
|
||||
|
|
|
@ -83,6 +83,7 @@ export const stepAboutDefaultValue: AboutStepRule = {
|
|||
isBuildingBlock: false,
|
||||
severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false },
|
||||
riskScore: { value: 21, mapping: [], isMappingChecked: false },
|
||||
investigationFields: [],
|
||||
references: [''],
|
||||
falsePositives: [''],
|
||||
license: '',
|
||||
|
|
|
@ -55,6 +55,7 @@ const contextValue: LeftPanelContext = {
|
|||
scopeId: '',
|
||||
browserFields: null,
|
||||
searchHit: undefined,
|
||||
investigationFields: [],
|
||||
};
|
||||
|
||||
const renderCorrelationDetails = () => {
|
||||
|
|
|
@ -108,7 +108,8 @@ const columns: Array<EuiBasicTableColumn<unknown>> = [
|
|||
* Prevalence table displayed in the document details expandable flyout left section under the Insights tab
|
||||
*/
|
||||
export const PrevalenceDetails: React.FC = () => {
|
||||
const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId } = useLeftPanelContext();
|
||||
const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId, investigationFields } =
|
||||
useLeftPanelContext();
|
||||
|
||||
const data = useMemo(() => {
|
||||
const summaryRows = getSummaryRows({
|
||||
|
@ -116,6 +117,7 @@ export const PrevalenceDetails: React.FC = () => {
|
|||
data: dataFormattedForFieldBrowser || [],
|
||||
eventId,
|
||||
scopeId,
|
||||
investigationFields,
|
||||
isReadOnly: false,
|
||||
});
|
||||
|
||||
|
@ -137,7 +139,7 @@ export const PrevalenceDetails: React.FC = () => {
|
|||
userPrevalence: fields,
|
||||
};
|
||||
});
|
||||
}, [browserFields, dataFormattedForFieldBrowser, eventId, scopeId]);
|
||||
}, [browserFields, investigationFields, dataFormattedForFieldBrowser, eventId, scopeId]);
|
||||
|
||||
if (!eventId || !dataFormattedForFieldBrowser || !browserFields || !data || data.length === 0) {
|
||||
return (
|
||||
|
|
|
@ -15,12 +15,16 @@ import type { LeftPanelProps } from '.';
|
|||
import type { GetFieldsData } from '../../common/hooks/use_get_fields_data';
|
||||
import { useGetFieldsData } from '../../common/hooks/use_get_fields_data';
|
||||
import { useTimelineEventsDetails } from '../../timelines/containers/details';
|
||||
import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers';
|
||||
import {
|
||||
getAlertIndexAlias,
|
||||
useBasicDataFromDetailsData,
|
||||
} from '../../timelines/components/side_panel/event_details/helpers';
|
||||
import { useSpaceId } from '../../common/hooks/use_space_id';
|
||||
import { useRouteSpy } from '../../common/utils/route/use_route_spy';
|
||||
import { SecurityPageName } from '../../../common/constants';
|
||||
import { SourcererScopeName } from '../../common/store/sourcerer/model';
|
||||
import { useSourcererDataView } from '../../common/containers/sourcerer';
|
||||
import { useRuleWithFallback } from '../../detection_engine/rule_management/logic/use_rule_with_fallback';
|
||||
|
||||
export interface LeftPanelContext {
|
||||
/**
|
||||
|
@ -51,6 +55,10 @@ export interface LeftPanelContext {
|
|||
* The actual raw document object
|
||||
*/
|
||||
searchHit: SearchHit | undefined;
|
||||
/**
|
||||
* User defined fields to highlight (defined on the rule)
|
||||
*/
|
||||
investigationFields: string[];
|
||||
/**
|
||||
* Retrieves searchHit values for the provided field
|
||||
*/
|
||||
|
@ -83,6 +91,8 @@ export const LeftPanelProvider = ({ id, indexName, scopeId, children }: LeftPane
|
|||
skip: !id,
|
||||
});
|
||||
const getFieldsData = useGetFieldsData(searchHit?.fields);
|
||||
const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
|
||||
const { rule: maybeRule } = useRuleWithFallback(ruleId);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() =>
|
||||
|
@ -95,6 +105,7 @@ export const LeftPanelProvider = ({ id, indexName, scopeId, children }: LeftPane
|
|||
dataAsNestedObject,
|
||||
dataFormattedForFieldBrowser,
|
||||
searchHit,
|
||||
investigationFields: maybeRule?.investigation_fields ?? [],
|
||||
getFieldsData,
|
||||
}
|
||||
: undefined,
|
||||
|
@ -103,10 +114,11 @@ export const LeftPanelProvider = ({ id, indexName, scopeId, children }: LeftPane
|
|||
indexName,
|
||||
scopeId,
|
||||
sourcererDataView.browserFields,
|
||||
dataFormattedForFieldBrowser,
|
||||
getFieldsData,
|
||||
dataAsNestedObject,
|
||||
dataFormattedForFieldBrowser,
|
||||
searchHit,
|
||||
maybeRule?.investigation_fields,
|
||||
getFieldsData,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -75,6 +75,7 @@ describe('useThreatIntelligenceDetails', () => {
|
|||
_index: 'testIndex',
|
||||
},
|
||||
dataAsNestedObject: null,
|
||||
investigationFields: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -47,4 +47,5 @@ export const mockContextValue: LeftPanelContext = {
|
|||
dataAsNestedObject: {
|
||||
_id: 'testId',
|
||||
},
|
||||
investigationFields: [],
|
||||
};
|
||||
|
|
|
@ -13,10 +13,16 @@ import { HighlightedFields } from './highlighted_fields';
|
|||
import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context';
|
||||
import { useHighlightedFields } from '../hooks/use_highlighted_fields';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback';
|
||||
|
||||
jest.mock('../hooks/use_highlighted_fields');
|
||||
jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallback');
|
||||
|
||||
describe('<HighlightedFields />', () => {
|
||||
beforeEach(() => {
|
||||
(useRuleWithFallback as jest.Mock).mockReturnValue({ investigation_fields: [] });
|
||||
});
|
||||
|
||||
it('should render the component', () => {
|
||||
const panelContextValue = {
|
||||
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
|
||||
|
|
|
@ -9,6 +9,8 @@ import type { FC } from 'react';
|
|||
import React from 'react';
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback';
|
||||
import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers';
|
||||
import { HighlightedFieldsCell } from './highlighted_fields_cell';
|
||||
import {
|
||||
CellActionsMode,
|
||||
|
@ -57,8 +59,13 @@ const columns: Array<EuiBasicTableColumn<UseHighlightedFieldsResult>> = [
|
|||
*/
|
||||
export const HighlightedFields: FC = () => {
|
||||
const { dataFormattedForFieldBrowser } = useRightPanelContext();
|
||||
const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
|
||||
const { rule: maybeRule } = useRuleWithFallback(ruleId);
|
||||
|
||||
const highlightedFields = useHighlightedFields({ dataFormattedForFieldBrowser });
|
||||
const highlightedFields = useHighlightedFields({
|
||||
dataFormattedForFieldBrowser,
|
||||
investigationFields: maybeRule?.investigation_fields ?? [],
|
||||
});
|
||||
|
||||
if (!dataFormattedForFieldBrowser || highlightedFields.length === 0) {
|
||||
return null;
|
||||
|
|
|
@ -23,8 +23,14 @@ import { PREVALENCE_TAB_ID } from '../../left/components/prevalence_details';
|
|||
* and the SummaryPanel component for data rendering.
|
||||
*/
|
||||
export const PrevalenceOverview: FC = () => {
|
||||
const { eventId, indexName, browserFields, dataFormattedForFieldBrowser, scopeId } =
|
||||
useRightPanelContext();
|
||||
const {
|
||||
eventId,
|
||||
indexName,
|
||||
browserFields,
|
||||
dataFormattedForFieldBrowser,
|
||||
scopeId,
|
||||
investigationFields,
|
||||
} = useRightPanelContext();
|
||||
const { openLeftPanel } = useExpandableFlyoutContext();
|
||||
|
||||
const goToCorrelationsTab = useCallback(() => {
|
||||
|
@ -46,6 +52,7 @@ export const PrevalenceOverview: FC = () => {
|
|||
eventId,
|
||||
browserFields,
|
||||
dataFormattedForFieldBrowser,
|
||||
investigationFields,
|
||||
scopeId,
|
||||
});
|
||||
|
||||
|
|
|
@ -10,9 +10,13 @@ import { css } from '@emotion/react';
|
|||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
|
||||
import type { SearchHit } from '../../../common/search_strategy';
|
||||
import { useTimelineEventsDetails } from '../../timelines/containers/details';
|
||||
import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers';
|
||||
import {
|
||||
getAlertIndexAlias,
|
||||
useBasicDataFromDetailsData,
|
||||
} from '../../timelines/components/side_panel/event_details/helpers';
|
||||
import { useSpaceId } from '../../common/hooks/use_space_id';
|
||||
import { useRouteSpy } from '../../common/utils/route/use_route_spy';
|
||||
import { SecurityPageName } from '../../../common/constants';
|
||||
|
@ -21,6 +25,7 @@ import { useSourcererDataView } from '../../common/containers/sourcerer';
|
|||
import type { RightPanelProps } from '.';
|
||||
import type { GetFieldsData } from '../../common/hooks/use_get_fields_data';
|
||||
import { useGetFieldsData } from '../../common/hooks/use_get_fields_data';
|
||||
import { useRuleWithFallback } from '../../detection_engine/rule_management/logic/use_rule_with_fallback';
|
||||
|
||||
export interface RightPanelContext {
|
||||
/**
|
||||
|
@ -51,6 +56,10 @@ export interface RightPanelContext {
|
|||
* The actual raw document object
|
||||
*/
|
||||
searchHit: SearchHit | undefined;
|
||||
/**
|
||||
* User defined fields to highlight (defined on the rule)
|
||||
*/
|
||||
investigationFields: string[];
|
||||
/**
|
||||
* Promise to trigger a data refresh
|
||||
*/
|
||||
|
@ -94,6 +103,8 @@ export const RightPanelProvider = ({
|
|||
skip: !id,
|
||||
});
|
||||
const getFieldsData = useGetFieldsData(searchHit?.fields);
|
||||
const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
|
||||
const { rule: maybeRule } = useRuleWithFallback(ruleId);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() =>
|
||||
|
@ -106,12 +117,14 @@ export const RightPanelProvider = ({
|
|||
dataAsNestedObject,
|
||||
dataFormattedForFieldBrowser,
|
||||
searchHit,
|
||||
investigationFields: maybeRule?.investigation_fields ?? [],
|
||||
refetchFlyoutData,
|
||||
getFieldsData,
|
||||
}
|
||||
: undefined,
|
||||
[
|
||||
id,
|
||||
maybeRule,
|
||||
indexName,
|
||||
scopeId,
|
||||
sourcererDataView.browserFields,
|
||||
|
|
|
@ -19,6 +19,10 @@ export interface UseHighlightedFieldsParams {
|
|||
* An array of field objects with category and value
|
||||
*/
|
||||
dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null;
|
||||
/**
|
||||
* An array of fields user has selected to highlight, defined on rule
|
||||
*/
|
||||
investigationFields?: string[];
|
||||
}
|
||||
|
||||
export interface UseHighlightedFieldsResult {
|
||||
|
@ -43,6 +47,7 @@ export interface UseHighlightedFieldsResult {
|
|||
*/
|
||||
export const useHighlightedFields = ({
|
||||
dataFormattedForFieldBrowser,
|
||||
investigationFields,
|
||||
}: UseHighlightedFieldsParams): UseHighlightedFieldsResult[] => {
|
||||
if (!dataFormattedForFieldBrowser) return [];
|
||||
|
||||
|
@ -61,6 +66,7 @@ export const useHighlightedFields = ({
|
|||
{ category: 'kibana', field: ALERT_RULE_TYPE },
|
||||
dataFormattedForFieldBrowser
|
||||
);
|
||||
|
||||
const eventRuleType = Array.isArray(eventRuleTypeField?.originalValue)
|
||||
? eventRuleTypeField?.originalValue?.[0]
|
||||
: eventRuleTypeField?.originalValue;
|
||||
|
@ -69,6 +75,7 @@ export const useHighlightedFields = ({
|
|||
eventCategories,
|
||||
eventCode,
|
||||
eventRuleType,
|
||||
highlightedFieldsOverride: investigationFields ?? [],
|
||||
});
|
||||
|
||||
return tableFields.reduce<UseHighlightedFieldsResult[]>((acc, field) => {
|
||||
|
|
|
@ -29,6 +29,10 @@ export interface UsePrevalenceParams {
|
|||
* Maintain backwards compatibility // TODO remove when possible
|
||||
*/
|
||||
scopeId: string;
|
||||
/**
|
||||
* User defined fields to highlight (defined on rule)
|
||||
*/
|
||||
investigationFields?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -41,6 +45,7 @@ export const usePrevalence = ({
|
|||
eventId,
|
||||
browserFields,
|
||||
dataFormattedForFieldBrowser,
|
||||
investigationFields,
|
||||
scopeId,
|
||||
}: UsePrevalenceParams): ReactElement[] => {
|
||||
// retrieves the highlighted fields
|
||||
|
@ -49,11 +54,12 @@ export const usePrevalence = ({
|
|||
getSummaryRows({
|
||||
browserFields: browserFields || {},
|
||||
data: dataFormattedForFieldBrowser || [],
|
||||
investigationFields: investigationFields || [],
|
||||
eventId,
|
||||
scopeId,
|
||||
isReadOnly: false,
|
||||
}),
|
||||
[browserFields, dataFormattedForFieldBrowser, eventId, scopeId]
|
||||
[browserFields, investigationFields, dataFormattedForFieldBrowser, eventId, scopeId]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
|
|
|
@ -20,5 +20,6 @@ export const mockContextValue: RightPanelContext = {
|
|||
browserFields: null,
|
||||
dataAsNestedObject: null,
|
||||
searchHit: undefined,
|
||||
investigationFields: [],
|
||||
refetchFlyoutData: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -100,4 +100,5 @@ export const getOutputRuleAlertForRest = (): RuleResponse => ({
|
|||
namespace: undefined,
|
||||
data_view_id: undefined,
|
||||
alert_suppression: undefined,
|
||||
investigation_fields: [],
|
||||
});
|
||||
|
|
|
@ -44,6 +44,7 @@ describe('schedule_notification_actions', () => {
|
|||
responseActions: [],
|
||||
riskScore: 80,
|
||||
riskScoreMapping: [],
|
||||
investigationFields: [],
|
||||
ruleNameOverride: undefined,
|
||||
dataViewId: undefined,
|
||||
outputIndex: 'output-1',
|
||||
|
|
|
@ -64,6 +64,7 @@ describe('schedule_throttle_notification_actions', () => {
|
|||
requiredFields: [],
|
||||
setup: '',
|
||||
alertSuppression: undefined,
|
||||
investigationFields: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ describe('duplicateRule', () => {
|
|||
timestampOverrideFallbackDisabled: undefined,
|
||||
dataViewId: undefined,
|
||||
alertSuppression: undefined,
|
||||
investigationFields: undefined,
|
||||
},
|
||||
schedule: {
|
||||
interval: '5m',
|
||||
|
|
|
@ -45,6 +45,7 @@ export const updateRules = async ({
|
|||
ruleId: existingRule.params.ruleId,
|
||||
falsePositives: ruleUpdate.false_positives ?? [],
|
||||
from: ruleUpdate.from ?? 'now-6m',
|
||||
investigationFields: ruleUpdate.investigation_fields ?? [],
|
||||
// Unlike the create route, immutable comes from the existing rule here
|
||||
immutable: existingRule.params.immutable,
|
||||
license: ruleUpdate.license,
|
||||
|
|
|
@ -136,6 +136,7 @@ describe('getExportAll', () => {
|
|||
note: '# Investigative notes',
|
||||
version: 1,
|
||||
exceptions_list: getListArrayMock(),
|
||||
investigation_fields: [],
|
||||
});
|
||||
expect(detailsJson).toEqual({
|
||||
exported_exception_list_count: 1,
|
||||
|
@ -319,6 +320,7 @@ describe('getExportAll', () => {
|
|||
version: 1,
|
||||
revision: 0,
|
||||
exceptions_list: getListArrayMock(),
|
||||
investigation_fields: [],
|
||||
});
|
||||
expect(detailsJson).toEqual({
|
||||
exported_exception_list_count: 1,
|
||||
|
|
|
@ -132,6 +132,7 @@ describe('get_export_by_object_ids', () => {
|
|||
note: '# Investigative notes',
|
||||
version: 1,
|
||||
exceptions_list: getListArrayMock(),
|
||||
investigation_fields: [],
|
||||
},
|
||||
exportDetails: {
|
||||
exported_exception_list_count: 0,
|
||||
|
@ -327,6 +328,7 @@ describe('get_export_by_object_ids', () => {
|
|||
version: 1,
|
||||
revision: 0,
|
||||
exceptions_list: getListArrayMock(),
|
||||
investigation_fields: [],
|
||||
});
|
||||
expect(detailsJson).toEqual({
|
||||
exported_exception_list_count: 0,
|
||||
|
@ -523,6 +525,7 @@ describe('get_export_by_object_ids', () => {
|
|||
namespace: undefined,
|
||||
data_view_id: undefined,
|
||||
alert_suppression: undefined,
|
||||
investigation_fields: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -409,6 +409,7 @@ export const convertPatchAPIToInternalSchema = (
|
|||
description: nextParams.description ?? existingParams.description,
|
||||
ruleId: existingParams.ruleId,
|
||||
falsePositives: nextParams.false_positives ?? existingParams.falsePositives,
|
||||
investigationFields: nextParams.investigation_fields ?? existingParams.investigationFields,
|
||||
from: nextParams.from ?? existingParams.from,
|
||||
immutable: existingParams.immutable,
|
||||
license: nextParams.license ?? existingParams.license,
|
||||
|
@ -470,6 +471,7 @@ export const convertCreateAPIToInternalSchema = (
|
|||
description: input.description,
|
||||
ruleId: newRuleId,
|
||||
falsePositives: input.false_positives ?? [],
|
||||
investigationFields: input.investigation_fields ?? [],
|
||||
from: input.from ?? 'now-6m',
|
||||
immutable,
|
||||
license: input.license,
|
||||
|
@ -619,6 +621,7 @@ export const commonParamsCamelToSnake = (params: BaseRuleParams) => {
|
|||
rule_name_override: params.ruleNameOverride,
|
||||
timestamp_override: params.timestampOverride,
|
||||
timestamp_override_fallback_disabled: params.timestampOverrideFallbackDisabled,
|
||||
investigation_fields: params.investigationFields,
|
||||
author: params.author,
|
||||
false_positives: params.falsePositives,
|
||||
from: params.from,
|
||||
|
|
|
@ -78,6 +78,7 @@ export const ruleOutput = (): RuleResponse => ({
|
|||
data_view_id: undefined,
|
||||
saved_id: undefined,
|
||||
alert_suppression: undefined,
|
||||
investigation_fields: [],
|
||||
});
|
||||
|
||||
describe('validate', () => {
|
||||
|
|
|
@ -48,6 +48,7 @@ const getBaseRuleParams = (): BaseRuleParams => {
|
|||
timelineTitle: 'some-timeline-title',
|
||||
timestampOverride: undefined,
|
||||
timestampOverrideFallbackDisabled: undefined,
|
||||
investigationFields: [],
|
||||
meta: {
|
||||
someMeta: 'someField',
|
||||
},
|
||||
|
|
|
@ -57,6 +57,7 @@ import {
|
|||
RuleAuthorArray,
|
||||
RuleDescription,
|
||||
RuleFalsePositiveArray,
|
||||
RuleCustomHighlightedFieldArray,
|
||||
RuleFilterArray,
|
||||
RuleLicense,
|
||||
RuleMetadata,
|
||||
|
@ -97,6 +98,7 @@ export const baseRuleParams = t.exact(
|
|||
falsePositives: RuleFalsePositiveArray,
|
||||
from: RuleIntervalFrom,
|
||||
ruleId: RuleSignatureId,
|
||||
investigationFields: t.union([RuleCustomHighlightedFieldArray, t.undefined]),
|
||||
immutable: IsRuleImmutable,
|
||||
license: t.union([RuleLicense, t.undefined]),
|
||||
outputIndex: AlertsIndex,
|
||||
|
|
|
@ -523,6 +523,7 @@ export const sampleSignalHit = (): SignalHit => ({
|
|||
filters: undefined,
|
||||
saved_id: undefined,
|
||||
alert_suppression: undefined,
|
||||
investigation_fields: undefined,
|
||||
},
|
||||
depth: 1,
|
||||
},
|
||||
|
|
|
@ -165,6 +165,7 @@ describe('buildAlert', () => {
|
|||
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
|
||||
query: 'user.name: root or user.name: admin',
|
||||
filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }],
|
||||
investigation_fields: [],
|
||||
},
|
||||
[ALERT_RULE_INDICES]: completeRule.ruleParams.index,
|
||||
...flattenWithPrefix(ALERT_RULE_NAMESPACE, {
|
||||
|
@ -358,6 +359,7 @@ describe('buildAlert', () => {
|
|||
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
|
||||
query: 'user.name: root or user.name: admin',
|
||||
filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }],
|
||||
investigation_fields: [],
|
||||
},
|
||||
...flattenWithPrefix(ALERT_RULE_NAMESPACE, {
|
||||
actions: [],
|
||||
|
|
|
@ -31106,7 +31106,6 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "Sélectionner un champ pour vérifier la cardinalité",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel": "Seuil",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.licenseWarning": "La suppression d'alertes est activée avec la licence Platinum ou supérieure",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.placeholderText": "Sélectionner un champ",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByDurationValueLabel": "Supprimer les alertes pour",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabel": "Supprimer les alertes par",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabelAppend": "Facultatif (version d'évaluation technique)",
|
||||
|
|
|
@ -31105,7 +31105,6 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "カーディナリティを確認するフィールドを選択します",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel": "しきい値",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.licenseWarning": "アラートの非表示は、プラチナライセンス以上で有効です",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.placeholderText": "フィールドを選択",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByDurationValueLabel": "アラートを非表示",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabel": "アラートを非表示",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabelAppend": "任意(テクニカルプレビュー)",
|
||||
|
|
|
@ -31101,7 +31101,6 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "选择字段以检查基数",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel": "阈值",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.licenseWarning": "告警阻止通过白金级或更高级许可证启用",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.placeholderText": "选择字段",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByDurationValueLabel": "阻止以下项的告警",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabel": "阻止告警的依据",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabelAppend": "可选(技术预览)",
|
||||
|
|
|
@ -76,6 +76,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
author: [],
|
||||
created_by: 'elastic',
|
||||
description: 'Simple Rule Query',
|
||||
investigation_fields: [],
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
from: 'now-6m',
|
||||
|
|
|
@ -207,6 +207,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
risk_score_mapping: [],
|
||||
name: 'Simple Rule Query',
|
||||
query: 'user.name: root or user.name: admin',
|
||||
investigation_fields: [],
|
||||
references: [],
|
||||
related_integrations: [],
|
||||
required_fields: [],
|
||||
|
|
|
@ -742,5 +742,6 @@ function expectToMatchRuleSchema(obj: RuleResponse): void {
|
|||
index: expect.arrayContaining([]),
|
||||
query: expect.any(String),
|
||||
actions: expect.arrayContaining([]),
|
||||
investigation_fields: expect.arrayContaining([]),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -426,6 +426,28 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
message: 'rule_id: "fake_id" not found',
|
||||
});
|
||||
});
|
||||
|
||||
describe('investigation_fields', () => {
|
||||
it('should overwrite investigation_fields value on update - non additive', async () => {
|
||||
await createRule(supertest, log, {
|
||||
...getSimpleRule('rule-1'),
|
||||
investigation_fields: ['blob', 'boop'],
|
||||
});
|
||||
|
||||
const rulePatch = {
|
||||
rule_id: 'rule-1',
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
};
|
||||
|
||||
const { body } = await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(rulePatch)
|
||||
.expect(200);
|
||||
|
||||
expect(body.investigation_fields).to.eql(['foo', 'bar']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('patch per-action frequencies', () => {
|
||||
|
|
|
@ -841,6 +841,28 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('investigation_fields', () => {
|
||||
it('should overwrite investigation_fields value on update - non additive', async () => {
|
||||
await createRule(supertest, log, {
|
||||
...getSimpleRule('rule-1'),
|
||||
investigation_fields: ['blob', 'boop'],
|
||||
});
|
||||
|
||||
const ruleUpdate = {
|
||||
...getSimpleRuleUpdate('rule-1'),
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
};
|
||||
|
||||
const { body } = await supertest
|
||||
.put(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(ruleUpdate)
|
||||
.expect(200);
|
||||
|
||||
expect(body.investigation_fields).to.eql(['foo', 'bar']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -337,6 +337,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
max_signals: 100,
|
||||
risk_score_mapping: [],
|
||||
severity_mapping: [],
|
||||
investigation_fields: [],
|
||||
threat: [],
|
||||
to: 'now',
|
||||
references: [],
|
||||
|
@ -512,6 +513,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
related_integrations: [],
|
||||
required_fields: [],
|
||||
setup: '',
|
||||
investigation_fields: [],
|
||||
},
|
||||
'kibana.alert.rule.actions': [],
|
||||
'kibana.alert.rule.created_by': 'elastic',
|
||||
|
|
|
@ -146,6 +146,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
to: 'now',
|
||||
type: 'machine_learning',
|
||||
version: 1,
|
||||
investigation_fields: [],
|
||||
},
|
||||
[ALERT_DEPTH]: 1,
|
||||
[ALERT_REASON]: `event with process store, by root on mothra created critical alert Test ML rule.`,
|
||||
|
|
|
@ -198,6 +198,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
history_window_start: '2019-01-19T20:42:00.000Z',
|
||||
index: ['auditbeat-*'],
|
||||
language: 'kuery',
|
||||
investigation_fields: [],
|
||||
},
|
||||
'kibana.alert.rule.actions': [],
|
||||
'kibana.alert.rule.author': [],
|
||||
|
|
|
@ -102,4 +102,5 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial<RuleResponse> =
|
|||
related_integrations: [],
|
||||
required_fields: [],
|
||||
setup: '',
|
||||
investigation_fields: [],
|
||||
});
|
||||
|
|
|
@ -60,6 +60,7 @@ export const getMockSharedResponseSchema = (
|
|||
timestamp_override: undefined,
|
||||
timestamp_override_fallback_disabled: undefined,
|
||||
namespace: undefined,
|
||||
investigation_fields: [],
|
||||
});
|
||||
|
||||
const getQueryRuleOutput = (ruleId = 'rule-1', enabled = false): RuleResponse => ({
|
||||
|
|
|
@ -42,7 +42,8 @@ import {
|
|||
} from '../../../screens/exceptions';
|
||||
import { goToEndpointExceptionsTab } from '../../../tasks/rule_details';
|
||||
|
||||
describe(
|
||||
// See https://github.com/elastic/kibana/issues/163967
|
||||
describe.skip(
|
||||
'Endpoint Exceptions workflows from Alert',
|
||||
{ tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] },
|
||||
() => {
|
||||
|
|
|
@ -39,7 +39,8 @@ import {
|
|||
} from '../../../../screens/exceptions';
|
||||
import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule';
|
||||
|
||||
describe(
|
||||
// See https://github.com/elastic/kibana/issues/163967
|
||||
describe.skip(
|
||||
'Auto populate exception with Alert data',
|
||||
{ tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] },
|
||||
() => {
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule';
|
||||
import {
|
||||
addExceptionFromFirstAlert,
|
||||
goToClosedAlertsOnRuleDetailsPage,
|
||||
waitForAlerts,
|
||||
} from '../../../../tasks/alerts';
|
||||
import { deleteAlertsAndRules, postDataView } from '../../../../tasks/common';
|
||||
import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule';
|
||||
import { login, visitWithoutDateRange } from '../../../../tasks/login';
|
||||
import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../../urls/navigation';
|
||||
import { goToRuleDetails } from '../../../../tasks/alerts_detection_rules';
|
||||
|
@ -27,6 +27,7 @@ import {
|
|||
submitNewExceptionItem,
|
||||
} from '../../../../tasks/exceptions';
|
||||
|
||||
// See https://github.com/elastic/kibana/issues/163967
|
||||
describe('Close matching Alerts ', () => {
|
||||
const newRule = getNewRule();
|
||||
const ITEM_NAME = 'Sample Exception Item';
|
||||
|
|
|
@ -532,6 +532,7 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response<RuleResponse
|
|||
immutable,
|
||||
related_integrations: relatedIntegrations,
|
||||
setup,
|
||||
investigation_fields: investigationFields,
|
||||
} = ruleResponse.body;
|
||||
|
||||
let query: string | undefined;
|
||||
|
@ -558,6 +559,7 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response<RuleResponse
|
|||
severity,
|
||||
note,
|
||||
output_index: '',
|
||||
investigation_fields: investigationFields,
|
||||
author,
|
||||
false_positives: falsePositives,
|
||||
from,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue