mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[DE] - Investigation fields followup (#164133)
## Summary Updates typing for new investigation_fields.
This commit is contained in:
parent
fc6034a18c
commit
4f87b43940
45 changed files with 317 additions and 98 deletions
|
@ -20,7 +20,6 @@ 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',
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import * as t from 'io-ts';
|
||||
import { listArray } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { NonEmptyString, version, UUID } from '@kbn/securitysolution-io-ts-types';
|
||||
import { NonEmptyString, version, UUID, NonEmptyArray } from '@kbn/securitysolution-io-ts-types';
|
||||
import { max_signals, threat } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
|
||||
export type RuleObjectId = t.TypeOf<typeof RuleObjectId>;
|
||||
|
@ -55,14 +55,6 @@ 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?
|
||||
|
||||
|
@ -265,3 +257,32 @@ export const RelatedIntegration = t.exact(
|
|||
*/
|
||||
export type RelatedIntegrationArray = t.TypeOf<typeof RelatedIntegrationArray>;
|
||||
export const RelatedIntegrationArray = t.array(RelatedIntegration);
|
||||
|
||||
/**
|
||||
* Schema for fields relating to investigation fields, these are user defined fields we use to highlight
|
||||
* in various features in the UI such as alert details flyout and exceptions auto-population from alert.
|
||||
* Added in PR #163235
|
||||
* Right now we only have a single field but anticipate adding more related fields to store various
|
||||
* configuration states such as `override` - where a user might say if they want only these fields to
|
||||
* display, or if they want these fields + the fields we select. When expanding this field, it may look
|
||||
* something like:
|
||||
* export const investigationFields = t.intersection([
|
||||
* t.exact(
|
||||
* t.type({
|
||||
* field_names: NonEmptyArray(NonEmptyString),
|
||||
* })
|
||||
* ),
|
||||
* t.exact(
|
||||
* t.partial({
|
||||
* overide: t.boolean,
|
||||
* })
|
||||
* ),
|
||||
* ]);
|
||||
*
|
||||
*/
|
||||
export type InvestigationFields = t.TypeOf<typeof InvestigationFields>;
|
||||
export const InvestigationFields = t.exact(
|
||||
t.type({
|
||||
field_names: NonEmptyArray(NonEmptyString),
|
||||
})
|
||||
);
|
||||
|
|
|
@ -1290,10 +1290,38 @@ describe('rules schema', () => {
|
|||
expect(getPaths(left(message.errors))).toEqual(['invalid keys "data_view_id"']);
|
||||
});
|
||||
|
||||
test('You can optionally send in an array of investigation_fields', () => {
|
||||
test('You can omit investigation_fields', () => {
|
||||
// getCreateRulesSchemaMock doesn't include investigation_fields
|
||||
const payload: RuleCreateProps = getCreateRulesSchemaMock();
|
||||
|
||||
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 pass empty object for investigation_fields', () => {
|
||||
const payload: Omit<RuleCreateProps, 'investigation_fields'> & {
|
||||
investigation_fields: unknown;
|
||||
} = {
|
||||
...getCreateRulesSchemaMock(),
|
||||
investigation_fields: {},
|
||||
};
|
||||
|
||||
const decoded = RuleCreateProps.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "undefined" supplied to "investigation_fields,field_names"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('You can send in investigation_fields', () => {
|
||||
const payload: RuleCreateProps = {
|
||||
...getCreateRulesSchemaMock(),
|
||||
investigation_fields: ['field1', 'field2'],
|
||||
investigation_fields: { field_names: ['field1', 'field2'] },
|
||||
};
|
||||
|
||||
const decoded = RuleCreateProps.decode(payload);
|
||||
|
@ -1303,19 +1331,49 @@ describe('rules schema', () => {
|
|||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('You cannot send in an array of investigation_fields that are numbers', () => {
|
||||
test('You cannot send in an empty array of investigation_fields.field_names', () => {
|
||||
const payload = {
|
||||
...getCreateRulesSchemaMock(),
|
||||
investigation_fields: [0, 1, 2],
|
||||
investigation_fields: { field_names: [] },
|
||||
};
|
||||
|
||||
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"',
|
||||
'Invalid value "[]" supplied to "investigation_fields,field_names"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('You cannot send in an array of investigation_fields.field_names that are numbers', () => {
|
||||
const payload = {
|
||||
...getCreateRulesSchemaMock(),
|
||||
investigation_fields: { field_names: [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,field_names"',
|
||||
'Invalid value "1" supplied to "investigation_fields,field_names"',
|
||||
'Invalid value "2" supplied to "investigation_fields,field_names"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('You cannot send in investigation_fields without specifying fields', () => {
|
||||
const payload = {
|
||||
...getCreateRulesSchemaMock(),
|
||||
investigation_fields: { foo: true },
|
||||
};
|
||||
|
||||
const decoded = RuleCreateProps.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "undefined" supplied to "investigation_fields,field_names"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
|
|
@ -234,27 +234,17 @@ describe('Rule response schema', () => {
|
|||
});
|
||||
|
||||
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'];
|
||||
payload.investigation_fields = { field_names: ['foo', 'bar'] };
|
||||
|
||||
const decoded = RuleResponse.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
const expected = { ...getRulesSchemaMock(), investigation_fields: ['foo', 'bar'] };
|
||||
const expected = {
|
||||
...getRulesSchemaMock(),
|
||||
investigation_fields: { field_names: ['foo', 'bar'] },
|
||||
};
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expected);
|
||||
|
@ -275,6 +265,42 @@ describe('Rule response schema', () => {
|
|||
expect(message.schema).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it should validate "investigation_fields" not in schema', () => {
|
||||
const payload: RuleResponse = {
|
||||
...getRulesSchemaMock(),
|
||||
investigation_fields: undefined,
|
||||
};
|
||||
|
||||
delete payload.investigation_fields;
|
||||
|
||||
const decoded = RuleResponse.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
const expected = getRulesSchemaMock();
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it should NOT validate an empty array for "investigation_fields.field_names"', () => {
|
||||
const payload: RuleResponse = {
|
||||
...getRulesSchemaMock(),
|
||||
investigation_fields: {
|
||||
field_names: [],
|
||||
},
|
||||
};
|
||||
|
||||
const decoded = RuleResponse.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "[]" supplied to "investigation_fields,field_names"',
|
||||
'Invalid value "{"field_names":[]}" supplied to "investigation_fields"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should NOT validate a string for "investigation_fields"', () => {
|
||||
const payload: Omit<RuleResponse, 'investigation_fields'> & {
|
||||
investigation_fields: string;
|
||||
|
|
|
@ -53,7 +53,7 @@ import {
|
|||
RelatedIntegrationArray,
|
||||
RequiredFieldArray,
|
||||
RuleAuthorArray,
|
||||
RuleCustomHighlightedFieldArray,
|
||||
InvestigationFields,
|
||||
RuleDescription,
|
||||
RuleFalsePositiveArray,
|
||||
RuleFilterArray,
|
||||
|
@ -117,7 +117,7 @@ export const baseSchema = buildRuleSchemas({
|
|||
output_index: AlertsIndex,
|
||||
namespace: AlertsIndexNamespace,
|
||||
meta: RuleMetadata,
|
||||
investigation_fields: RuleCustomHighlightedFieldArray,
|
||||
investigation_fields: InvestigationFields,
|
||||
// Throttle
|
||||
throttle: RuleActionThrottle,
|
||||
},
|
||||
|
|
|
@ -289,7 +289,7 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
isReadOnly,
|
||||
}}
|
||||
goToTable={goToTableTab}
|
||||
investigationFields={maybeRule?.investigation_fields ?? []}
|
||||
investigationFields={maybeRule?.investigation_fields?.field_names ?? []}
|
||||
/>
|
||||
<EuiSpacer size="xl" />
|
||||
<Insights
|
||||
|
|
|
@ -555,7 +555,7 @@ describe('helpers', () => {
|
|||
severity_mapping: [],
|
||||
tags: ['tag1', 'tag2'],
|
||||
threat: getThreatMock(),
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
investigation_fields: { field_names: ['foo', 'bar'] },
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
@ -636,7 +636,7 @@ describe('helpers', () => {
|
|||
severity_mapping: [],
|
||||
tags: ['tag1', 'tag2'],
|
||||
threat: getThreatMock(),
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
investigation_fields: { field_names: ['foo', 'bar'] },
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
@ -661,7 +661,7 @@ describe('helpers', () => {
|
|||
severity_mapping: [],
|
||||
tags: ['tag1', 'tag2'],
|
||||
threat: getThreatMock(),
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
investigation_fields: { field_names: ['foo', 'bar'] },
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
@ -705,7 +705,7 @@ describe('helpers', () => {
|
|||
severity_mapping: [],
|
||||
tags: ['tag1', 'tag2'],
|
||||
threat: getThreatMock(),
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
investigation_fields: { field_names: ['foo', 'bar'] },
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
@ -758,7 +758,7 @@ describe('helpers', () => {
|
|||
],
|
||||
},
|
||||
],
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
investigation_fields: { field_names: ['foo', 'bar'] },
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
@ -787,13 +787,13 @@ describe('helpers', () => {
|
|||
threat: getThreatMock(),
|
||||
timestamp_override: 'event.ingest',
|
||||
timestamp_override_fallback_disabled: true,
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
investigation_fields: { field_names: ['foo', 'bar'] },
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns formatted object if investigation_fields is empty array', () => {
|
||||
test('returns formatted object if investigationFields is empty array', () => {
|
||||
const mockStepData: AboutStepRule = {
|
||||
...mockData,
|
||||
investigationFields: [],
|
||||
|
@ -817,7 +817,7 @@ describe('helpers', () => {
|
|||
timestamp_override: undefined,
|
||||
timestamp_override_fallback_disabled: undefined,
|
||||
threat: getThreatMock(),
|
||||
investigation_fields: [],
|
||||
investigation_fields: undefined,
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
|
@ -843,7 +843,7 @@ describe('helpers', () => {
|
|||
severity_mapping: [],
|
||||
tags: ['tag1', 'tag2'],
|
||||
threat: getThreatMock(),
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
investigation_fields: { field_names: ['foo', 'bar'] },
|
||||
threat_indicator_path: undefined,
|
||||
timestamp_override: undefined,
|
||||
timestamp_override_fallback_disabled: undefined,
|
||||
|
@ -855,7 +855,7 @@ describe('helpers', () => {
|
|||
test('returns formatted object if investigation_fields includes empty string', () => {
|
||||
const mockStepData: AboutStepRule = {
|
||||
...mockData,
|
||||
investigationFields: [' '],
|
||||
investigationFields: [' '],
|
||||
};
|
||||
const result = formatAboutStepData(mockStepData);
|
||||
const expected: AboutStepRuleJson = {
|
||||
|
@ -872,7 +872,7 @@ describe('helpers', () => {
|
|||
severity_mapping: [],
|
||||
tags: ['tag1', 'tag2'],
|
||||
threat: getThreatMock(),
|
||||
investigation_fields: [],
|
||||
investigation_fields: undefined,
|
||||
threat_indicator_path: undefined,
|
||||
timestamp_override: undefined,
|
||||
timestamp_override_fallback_disabled: undefined,
|
||||
|
|
|
@ -502,6 +502,7 @@ export const formatAboutStepData = (
|
|||
|
||||
const detectionExceptionLists =
|
||||
exceptionsList != null ? exceptionsList.filter((list) => list.type !== 'endpoint') : [];
|
||||
const isinvestigationFieldsEmpty = investigationFields.every((item) => isEmpty(item.trim()));
|
||||
|
||||
const resp = {
|
||||
author: author.filter((item) => !isEmpty(item)),
|
||||
|
@ -525,7 +526,9 @@ export const formatAboutStepData = (
|
|||
: {}),
|
||||
false_positives: falsePositives.filter((item) => !isEmpty(item)),
|
||||
references: references.filter((item) => !isEmpty(item)),
|
||||
investigation_fields: investigationFields.filter((item) => !isEmpty(item.trim())),
|
||||
investigation_fields: isinvestigationFieldsEmpty
|
||||
? undefined
|
||||
: { field_names: investigationFields },
|
||||
risk_score: riskScore.value,
|
||||
risk_score_mapping: riskScore.isMappingChecked
|
||||
? riskScore.mapping.filter((m) => m.field != null && m.field !== '')
|
||||
|
|
|
@ -671,7 +671,7 @@ describe('When the add exception modal is opened', () => {
|
|||
rules={[
|
||||
{
|
||||
...getRulesSchemaMock(),
|
||||
investigation_fields: ['foo.bar'],
|
||||
investigation_fields: { field_names: ['foo.bar'] },
|
||||
exceptions_list: [],
|
||||
} as Rule,
|
||||
]}
|
||||
|
|
|
@ -348,7 +348,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
|
|||
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 ?? [],
|
||||
ruleCustomHighlightedFields: rules?.[0]?.investigation_fields?.field_names ?? [],
|
||||
});
|
||||
if (populatedException) {
|
||||
setComment(i18n.ADD_RULE_EXCEPTION_FROM_ALERT_COMMENT(alertData._id));
|
||||
|
|
|
@ -283,10 +283,12 @@ const prepareAboutSectionListItems = (rule: RuleResponse): EuiDescriptionListPro
|
|||
});
|
||||
}
|
||||
|
||||
if (rule.investigation_fields && rule.investigation_fields.length > 0) {
|
||||
if (rule.investigation_fields && rule.investigation_fields.field_names.length > 0) {
|
||||
aboutSectionListItems.push({
|
||||
title: i18n.INVESTIGATION_FIELDS_FIELD_LABEL,
|
||||
description: <InvestigationFields investigationFields={rule.investigation_fields} />,
|
||||
description: (
|
||||
<InvestigationFields investigationFields={rule.investigation_fields.field_names} />
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ import {
|
|||
TimestampField,
|
||||
TimestampOverride,
|
||||
TimestampOverrideFallbackDisabled,
|
||||
RuleCustomHighlightedFieldArray,
|
||||
InvestigationFields,
|
||||
} from '../../../../common/api/detection_engine/model/rule_schema';
|
||||
|
||||
import type {
|
||||
|
@ -202,7 +202,7 @@ export const RuleSchema = t.intersection([
|
|||
version: RuleVersion,
|
||||
execution_summary: RuleExecutionSummary,
|
||||
alert_suppression: AlertSuppression,
|
||||
investigation_fields: RuleCustomHighlightedFieldArray,
|
||||
investigation_fields: InvestigationFields,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
@ -231,7 +231,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu
|
|||
isMappingChecked: riskScoreMapping.length > 0,
|
||||
},
|
||||
falsePositives,
|
||||
investigationFields: investigationFields ?? [],
|
||||
investigationFields: investigationFields?.field_names ?? [],
|
||||
threat: threat as Threats,
|
||||
threatIndicatorPath,
|
||||
};
|
||||
|
|
|
@ -34,6 +34,7 @@ import type {
|
|||
SetupGuide,
|
||||
TimestampOverride,
|
||||
AlertSuppressionMissingFields,
|
||||
InvestigationFields,
|
||||
} from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import type { SortOrder } from '../../../../../common/api/detection_engine';
|
||||
import type { EqlOptionsSelected } from '../../../../../common/search_strategy';
|
||||
|
@ -239,7 +240,7 @@ export interface AboutStepRuleJson {
|
|||
timestamp_override?: TimestampOverride;
|
||||
timestamp_override_fallback_disabled?: boolean;
|
||||
note?: string;
|
||||
investigation_fields?: string[];
|
||||
investigation_fields?: InvestigationFields;
|
||||
}
|
||||
|
||||
export interface ScheduleStepRuleJson {
|
||||
|
|
|
@ -105,7 +105,7 @@ export const LeftPanelProvider = ({ id, indexName, scopeId, children }: LeftPane
|
|||
dataAsNestedObject,
|
||||
dataFormattedForFieldBrowser,
|
||||
searchHit,
|
||||
investigationFields: maybeRule?.investigation_fields ?? [],
|
||||
investigationFields: maybeRule?.investigation_fields?.field_names ?? [],
|
||||
getFieldsData,
|
||||
}
|
||||
: undefined,
|
||||
|
|
|
@ -20,7 +20,7 @@ jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallbac
|
|||
|
||||
describe('<HighlightedFields />', () => {
|
||||
beforeEach(() => {
|
||||
(useRuleWithFallback as jest.Mock).mockReturnValue({ investigation_fields: [] });
|
||||
(useRuleWithFallback as jest.Mock).mockReturnValue({ investigation_fields: undefined });
|
||||
});
|
||||
|
||||
it('should render the component', () => {
|
||||
|
|
|
@ -91,7 +91,7 @@ export const HighlightedFields: FC = () => {
|
|||
|
||||
const highlightedFields = useHighlightedFields({
|
||||
dataFormattedForFieldBrowser,
|
||||
investigationFields: maybeRule?.investigation_fields ?? [],
|
||||
investigationFields: maybeRule?.investigation_fields?.field_names ?? [],
|
||||
});
|
||||
const items = useMemo(
|
||||
() => convertHighlightedFieldsToTableRow(highlightedFields, scopeId),
|
||||
|
|
|
@ -117,7 +117,7 @@ export const RightPanelProvider = ({
|
|||
dataAsNestedObject,
|
||||
dataFormattedForFieldBrowser,
|
||||
searchHit,
|
||||
investigationFields: maybeRule?.investigation_fields ?? [],
|
||||
investigationFields: maybeRule?.investigation_fields?.field_names ?? [],
|
||||
refetchFlyoutData,
|
||||
getFieldsData,
|
||||
}
|
||||
|
|
|
@ -100,5 +100,5 @@ export const getOutputRuleAlertForRest = (): RuleResponse => ({
|
|||
namespace: undefined,
|
||||
data_view_id: undefined,
|
||||
alert_suppression: undefined,
|
||||
investigation_fields: [],
|
||||
investigation_fields: undefined,
|
||||
});
|
||||
|
|
|
@ -44,7 +44,7 @@ describe('schedule_notification_actions', () => {
|
|||
responseActions: [],
|
||||
riskScore: 80,
|
||||
riskScoreMapping: [],
|
||||
investigationFields: [],
|
||||
investigationFields: undefined,
|
||||
ruleNameOverride: undefined,
|
||||
dataViewId: undefined,
|
||||
outputIndex: 'output-1',
|
||||
|
|
|
@ -45,7 +45,7 @@ export const updateRules = async ({
|
|||
ruleId: existingRule.params.ruleId,
|
||||
falsePositives: ruleUpdate.false_positives ?? [],
|
||||
from: ruleUpdate.from ?? 'now-6m',
|
||||
investigationFields: ruleUpdate.investigation_fields ?? [],
|
||||
investigationFields: ruleUpdate.investigation_fields,
|
||||
// Unlike the create route, immutable comes from the existing rule here
|
||||
immutable: existingRule.params.immutable,
|
||||
license: ruleUpdate.license,
|
||||
|
|
|
@ -136,7 +136,7 @@ describe('getExportAll', () => {
|
|||
note: '# Investigative notes',
|
||||
version: 1,
|
||||
exceptions_list: getListArrayMock(),
|
||||
investigation_fields: [],
|
||||
investigation_fields: undefined,
|
||||
});
|
||||
expect(detailsJson).toEqual({
|
||||
exported_exception_list_count: 1,
|
||||
|
@ -320,7 +320,7 @@ describe('getExportAll', () => {
|
|||
version: 1,
|
||||
revision: 0,
|
||||
exceptions_list: getListArrayMock(),
|
||||
investigation_fields: [],
|
||||
investigation_fields: undefined,
|
||||
});
|
||||
expect(detailsJson).toEqual({
|
||||
exported_exception_list_count: 1,
|
||||
|
|
|
@ -132,7 +132,7 @@ describe('get_export_by_object_ids', () => {
|
|||
note: '# Investigative notes',
|
||||
version: 1,
|
||||
exceptions_list: getListArrayMock(),
|
||||
investigation_fields: [],
|
||||
investigation_fields: undefined,
|
||||
},
|
||||
exportDetails: {
|
||||
exported_exception_list_count: 0,
|
||||
|
@ -328,7 +328,7 @@ describe('get_export_by_object_ids', () => {
|
|||
version: 1,
|
||||
revision: 0,
|
||||
exceptions_list: getListArrayMock(),
|
||||
investigation_fields: [],
|
||||
investigation_fields: undefined,
|
||||
});
|
||||
expect(detailsJson).toEqual({
|
||||
exported_exception_list_count: 0,
|
||||
|
@ -525,7 +525,7 @@ describe('get_export_by_object_ids', () => {
|
|||
namespace: undefined,
|
||||
data_view_id: undefined,
|
||||
alert_suppression: undefined,
|
||||
investigation_fields: [],
|
||||
investigation_fields: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -476,7 +476,7 @@ export const convertCreateAPIToInternalSchema = (
|
|||
description: input.description,
|
||||
ruleId: newRuleId,
|
||||
falsePositives: input.false_positives ?? [],
|
||||
investigationFields: input.investigation_fields ?? [],
|
||||
investigationFields: input.investigation_fields,
|
||||
from: input.from ?? DEFAULT_FROM,
|
||||
immutable,
|
||||
license: input.license,
|
||||
|
|
|
@ -78,7 +78,7 @@ export const ruleOutput = (): RuleResponse => ({
|
|||
data_view_id: undefined,
|
||||
saved_id: undefined,
|
||||
alert_suppression: undefined,
|
||||
investigation_fields: [],
|
||||
investigation_fields: undefined,
|
||||
});
|
||||
|
||||
describe('validate', () => {
|
||||
|
|
|
@ -48,7 +48,6 @@ const getBaseRuleParams = (): BaseRuleParams => {
|
|||
timelineTitle: 'some-timeline-title',
|
||||
timestampOverride: undefined,
|
||||
timestampOverrideFallbackDisabled: undefined,
|
||||
investigationFields: [],
|
||||
meta: {
|
||||
someMeta: 'someField',
|
||||
},
|
||||
|
@ -58,6 +57,7 @@ const getBaseRuleParams = (): BaseRuleParams => {
|
|||
relatedIntegrations: [],
|
||||
requiredFields: [],
|
||||
setup: '',
|
||||
investigationFields: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ import {
|
|||
RuleAuthorArray,
|
||||
RuleDescription,
|
||||
RuleFalsePositiveArray,
|
||||
RuleCustomHighlightedFieldArray,
|
||||
InvestigationFields,
|
||||
RuleFilterArray,
|
||||
RuleLicense,
|
||||
RuleMetadata,
|
||||
|
@ -98,7 +98,7 @@ export const baseRuleParams = t.exact(
|
|||
falsePositives: RuleFalsePositiveArray,
|
||||
from: RuleIntervalFrom,
|
||||
ruleId: RuleSignatureId,
|
||||
investigationFields: t.union([RuleCustomHighlightedFieldArray, t.undefined]),
|
||||
investigationFields: t.union([InvestigationFields, t.undefined]),
|
||||
immutable: IsRuleImmutable,
|
||||
license: t.union([RuleLicense, t.undefined]),
|
||||
outputIndex: AlertsIndex,
|
||||
|
|
|
@ -165,7 +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: [],
|
||||
investigation_fields: undefined,
|
||||
},
|
||||
[ALERT_RULE_INDICES]: completeRule.ruleParams.index,
|
||||
...flattenWithPrefix(ALERT_RULE_NAMESPACE, {
|
||||
|
@ -359,7 +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: [],
|
||||
investigation_fields: undefined,
|
||||
},
|
||||
...flattenWithPrefix(ALERT_RULE_NAMESPACE, {
|
||||
actions: [],
|
||||
|
|
|
@ -77,7 +77,6 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
author: [],
|
||||
created_by: 'elastic',
|
||||
description: 'Simple Rule Query',
|
||||
investigation_fields: [],
|
||||
enabled: true,
|
||||
false_positives: [],
|
||||
from: 'now-6m',
|
||||
|
|
|
@ -213,7 +213,6 @@ 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: [],
|
||||
|
|
|
@ -754,6 +754,5 @@ function expectToMatchRuleSchema(obj: RuleResponse): void {
|
|||
index: expect.arrayContaining([]),
|
||||
query: expect.any(String),
|
||||
actions: expect.arrayContaining([]),
|
||||
investigation_fields: expect.arrayContaining([]),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
deleteAllAlerts,
|
||||
getSimpleRule,
|
||||
getSimpleRuleAsNdjson,
|
||||
getRulesAsNdjson,
|
||||
getSimpleRuleOutput,
|
||||
getThresholdRuleForSignalTesting,
|
||||
getWebHookAction,
|
||||
|
@ -1868,6 +1869,21 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should import a rule with "investigation_fields', async () => {
|
||||
await supertest
|
||||
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.attach(
|
||||
'file',
|
||||
getRulesAsNdjson([
|
||||
{ ...getSimpleRule(), investigation_fields: { field_names: ['foo'] } },
|
||||
]),
|
||||
'rules.ndjson'
|
||||
)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -445,15 +445,15 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
describe('investigation_fields', () => {
|
||||
it('should overwrite investigation_fields value on update - non additive', async () => {
|
||||
it('should overwrite investigation_fields value on patch - non additive', async () => {
|
||||
await createRule(supertest, log, {
|
||||
...getSimpleRule('rule-1'),
|
||||
investigation_fields: ['blob', 'boop'],
|
||||
investigation_fields: { field_names: ['blob', 'boop'] },
|
||||
});
|
||||
|
||||
const rulePatch = {
|
||||
rule_id: 'rule-1',
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
investigation_fields: { field_names: ['foo', 'bar'] },
|
||||
};
|
||||
|
||||
const { body } = await supertest
|
||||
|
@ -463,7 +463,47 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
.send(rulePatch)
|
||||
.expect(200);
|
||||
|
||||
expect(body.investigation_fields).to.eql(['foo', 'bar']);
|
||||
expect(body.investigation_fields.field_names).to.eql(['foo', 'bar']);
|
||||
});
|
||||
|
||||
it('should not allow field to be unset', async () => {
|
||||
await createRule(supertest, log, {
|
||||
...getSimpleRule('rule-1'),
|
||||
investigation_fields: { field_names: ['blob', 'boop'] },
|
||||
});
|
||||
|
||||
const rulePatch = {
|
||||
rule_id: 'rule-1',
|
||||
investigation_fields: undefined,
|
||||
};
|
||||
|
||||
const { body } = await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(rulePatch)
|
||||
.expect(200);
|
||||
|
||||
expect(body.investigation_fields).to.eql({ field_names: ['blob', 'boop'] });
|
||||
});
|
||||
|
||||
it('should not unset investigation_fields if not specified in patch', async () => {
|
||||
await createRule(supertest, log, {
|
||||
...getSimpleRule('rule-1'),
|
||||
investigation_fields: { field_names: ['blob', 'boop'] },
|
||||
});
|
||||
|
||||
const rulePatch = {
|
||||
rule_id: 'rule-1',
|
||||
name: 'New name',
|
||||
};
|
||||
|
||||
const { body } = await supertest
|
||||
.patch(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(rulePatch)
|
||||
.expect(200);
|
||||
|
||||
expect(body.investigation_fields.field_names).to.eql(['blob', 'boop']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -870,12 +870,12 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
it('should overwrite investigation_fields value on update - non additive', async () => {
|
||||
await createRule(supertest, log, {
|
||||
...getSimpleRule('rule-1'),
|
||||
investigation_fields: ['blob', 'boop'],
|
||||
investigation_fields: { field_names: ['blob', 'boop'] },
|
||||
});
|
||||
|
||||
const ruleUpdate = {
|
||||
...getSimpleRuleUpdate('rule-1'),
|
||||
investigation_fields: ['foo', 'bar'],
|
||||
investigation_fields: { field_names: ['foo', 'bar'] },
|
||||
};
|
||||
|
||||
const { body } = await supertest
|
||||
|
@ -885,7 +885,27 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
.send(ruleUpdate)
|
||||
.expect(200);
|
||||
|
||||
expect(body.investigation_fields).to.eql(['foo', 'bar']);
|
||||
expect(body.investigation_fields.field_names).to.eql(['foo', 'bar']);
|
||||
});
|
||||
|
||||
it('should unset investigation_fields', async () => {
|
||||
await createRule(supertest, log, {
|
||||
...getSimpleRule('rule-1'),
|
||||
investigation_fields: { field_names: ['blob', 'boop'] },
|
||||
});
|
||||
|
||||
const ruleUpdate = {
|
||||
...getSimpleRuleUpdate('rule-1'),
|
||||
investigation_fields: undefined,
|
||||
};
|
||||
|
||||
const { body } = await supertest
|
||||
.put(DETECTION_ENGINE_RULES_URL)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(ruleUpdate)
|
||||
.expect(200);
|
||||
|
||||
expect(body.investigation_fields).to.eql(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -337,7 +337,6 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
max_signals: 100,
|
||||
risk_score_mapping: [],
|
||||
severity_mapping: [],
|
||||
investigation_fields: [],
|
||||
threat: [],
|
||||
to: 'now',
|
||||
references: [],
|
||||
|
@ -513,7 +512,6 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
related_integrations: [],
|
||||
required_fields: [],
|
||||
setup: '',
|
||||
investigation_fields: [],
|
||||
},
|
||||
'kibana.alert.rule.actions': [],
|
||||
'kibana.alert.rule.created_by': 'elastic',
|
||||
|
|
|
@ -146,7 +146,6 @@ 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,7 +198,6 @@ 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,5 +102,4 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial<RuleResponse> =
|
|||
related_integrations: [],
|
||||
required_fields: [],
|
||||
setup: '',
|
||||
investigation_fields: [],
|
||||
});
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Given an array of objects (assuming rules) this will return a ndjson buffer which is useful
|
||||
* for testing uploads.
|
||||
* @param rules Array of rules
|
||||
*/
|
||||
export const getRulesAsNdjson = (rules: unknown[]): Buffer => {
|
||||
const stringOfRules = rules.map((rule) => {
|
||||
return JSON.stringify(rule);
|
||||
});
|
||||
return Buffer.from(stringOfRules.join('\n'));
|
||||
};
|
|
@ -60,7 +60,7 @@ export const getMockSharedResponseSchema = (
|
|||
timestamp_override: undefined,
|
||||
timestamp_override_fallback_disabled: undefined,
|
||||
namespace: undefined,
|
||||
investigation_fields: [],
|
||||
investigation_fields: undefined,
|
||||
});
|
||||
|
||||
const getQueryRuleOutput = (ruleId = 'rule-1', enabled = false): RuleResponse => ({
|
||||
|
|
|
@ -46,6 +46,7 @@ export * from './get_query_signal_ids';
|
|||
export * from './get_query_signals_ids';
|
||||
export * from './get_query_signals_rule_id';
|
||||
export * from './get_rule';
|
||||
export * from './get_rules_as_ndjson';
|
||||
export * from './get_rule_for_signal_testing';
|
||||
export * from './get_rule_so_by_id';
|
||||
export * from './get_rule_for_signal_testing_with_timestamp_override';
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
COPY_ALERT_FLYOUT_LINK,
|
||||
JSON_TEXT,
|
||||
OVERVIEW_RULE,
|
||||
SUMMARY_VIEW,
|
||||
TABLE_CONTAINER,
|
||||
TABLE_ROWS,
|
||||
} from '../../../screens/alerts_details';
|
||||
|
@ -37,7 +38,7 @@ import { goToRuleDetails } from '../../../tasks/alerts_detection_rules';
|
|||
|
||||
describe('Alert details flyout', { tags: ['@ess', '@serverless'] }, () => {
|
||||
describe('Basic functions', () => {
|
||||
before(() => {
|
||||
beforeEach(() => {
|
||||
cleanKibana();
|
||||
login();
|
||||
disableExpandableFlyout();
|
||||
|
@ -48,6 +49,7 @@ describe('Alert details flyout', { tags: ['@ess', '@serverless'] }, () => {
|
|||
});
|
||||
|
||||
it('should update the table when status of the alert is updated', () => {
|
||||
cy.get(OVERVIEW_RULE).should('be.visible');
|
||||
cy.get(ALERTS_TABLE_COUNT).should('have.text', '2 alerts');
|
||||
cy.get(ALERT_SUMMARY_SEVERITY_DONUT_CHART).should('contain.text', '2alerts');
|
||||
expandFirstAlert();
|
||||
|
@ -61,7 +63,7 @@ describe('Alert details flyout', { tags: ['@ess', '@serverless'] }, () => {
|
|||
before(() => {
|
||||
cleanKibana();
|
||||
cy.task('esArchiverLoad', { archiveName: 'unmapped_fields' });
|
||||
createRule(getUnmappedRule());
|
||||
createRule({ ...getUnmappedRule(), investigation_fields: { field_names: ['event.kind'] } });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -76,6 +78,13 @@ describe('Alert details flyout', { tags: ['@ess', '@serverless'] }, () => {
|
|||
cy.task('esArchiverUnload', 'unmapped_fields');
|
||||
});
|
||||
|
||||
it('should display user and system defined highlighted fields', () => {
|
||||
cy.get(SUMMARY_VIEW)
|
||||
.should('be.visible')
|
||||
.and('contain.text', 'event.kind')
|
||||
.and('contain.text', 'Rule type');
|
||||
});
|
||||
|
||||
it('should display the unmapped field on the JSON view', () => {
|
||||
const expectedUnmappedValue = 'This is the unmapped field';
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ describe('Alert details expandable flyout left panel prevalence', () => {
|
|||
beforeEach(() => {
|
||||
cleanKibana();
|
||||
login();
|
||||
createRule(getNewRule());
|
||||
createRule({ ...getNewRule(), investigation_fields: { field_names: ['host.os.name'] } });
|
||||
visit(ALERTS_URL);
|
||||
waitForAlertsToPopulate();
|
||||
expandFirstAlertExpandableFlyout();
|
||||
|
@ -57,10 +57,12 @@ describe('Alert details expandable flyout left panel prevalence', () => {
|
|||
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE).should('be.visible');
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_TYPE_CELL)
|
||||
.should('contain.text', 'host.name')
|
||||
.should('contain.text', 'host.os.name')
|
||||
.and('contain.text', 'host.name')
|
||||
.and('contain.text', 'user.name');
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_NAME_CELL)
|
||||
.should('contain.text', 'siem-kibana')
|
||||
.should('contain.text', 'Mac OS X')
|
||||
.and('contain.text', 'siem-kibana')
|
||||
.and('contain.text', 'test');
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_ALERT_COUNT_CELL).should(
|
||||
'contain.text',
|
||||
|
|
|
@ -71,7 +71,7 @@ describe(
|
|||
'Alert details expandable flyout right panel overview tab',
|
||||
{ tags: ['@ess', '@brokenInServerless'] },
|
||||
() => {
|
||||
const rule = getNewRule();
|
||||
const rule = { ...getNewRule(), investigation_fields: { field_names: ['host.os.name'] } };
|
||||
|
||||
beforeEach(() => {
|
||||
cleanKibana();
|
||||
|
@ -177,7 +177,7 @@ describe(
|
|||
cy.get(DOCUMENT_DETAILS_FLYOUT_INVESTIGATION_TAB_CONTENT).scrollIntoView();
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INVESTIGATION_TAB_CONTENT).should('be.visible');
|
||||
|
||||
cy.log('highlighted fields');
|
||||
cy.log('highlighted fields section');
|
||||
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_HEADER_TITLE)
|
||||
.should('be.visible')
|
||||
|
@ -186,6 +186,17 @@ describe(
|
|||
'be.visible'
|
||||
);
|
||||
|
||||
cy.log('custom highlighted fields');
|
||||
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_FIELD_CELL)
|
||||
.should('be.visible')
|
||||
.and('contain.text', 'host.os.name');
|
||||
const customHighlightedField =
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_VALUE_CELL('Mac OS X');
|
||||
cy.get(customHighlightedField).should('be.visible').and('have.text', 'Mac OS X');
|
||||
|
||||
cy.log('system defined highlighted fields');
|
||||
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_FIELD_CELL)
|
||||
.should('be.visible')
|
||||
.and('contain.text', 'host.name');
|
||||
|
|
|
@ -45,7 +45,7 @@ export const OVERVIEW_STATUS = '[data-test-subj="eventDetails"] [data-test-subj=
|
|||
export const EVENT_DETAILS_ALERT_STATUS_POPOVER =
|
||||
'[data-test-subj="event-details-alertStatusPopover"]';
|
||||
|
||||
const SUMMARY_VIEW = '[data-test-subj="summary-view"]';
|
||||
export const SUMMARY_VIEW = '[data-test-subj="summary-view"]';
|
||||
|
||||
export const TABLE_CELL = '.euiTableRowCell';
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue