[DE] - Investigation fields followup (#164133)

## Summary

Updates typing for new investigation_fields.
This commit is contained in:
Yara Tercero 2023-08-29 05:41:24 -07:00 committed by GitHub
parent fc6034a18c
commit 4f87b43940
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 317 additions and 98 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 !== '')

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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', () => {

View file

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

View file

@ -117,7 +117,7 @@ export const RightPanelProvider = ({
dataAsNestedObject,
dataFormattedForFieldBrowser,
searchHit,
investigationFields: maybeRule?.investigation_fields ?? [],
investigationFields: maybeRule?.investigation_fields?.field_names ?? [],
refetchFlyoutData,
getFieldsData,
}

View file

@ -100,5 +100,5 @@ export const getOutputRuleAlertForRest = (): RuleResponse => ({
namespace: undefined,
data_view_id: undefined,
alert_suppression: undefined,
investigation_fields: [],
investigation_fields: undefined,
});

View file

@ -44,7 +44,7 @@ describe('schedule_notification_actions', () => {
responseActions: [],
riskScore: 80,
riskScoreMapping: [],
investigationFields: [],
investigationFields: undefined,
ruleNameOverride: undefined,
dataViewId: undefined,
outputIndex: 'output-1',

View file

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

View file

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

View file

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

View file

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

View file

@ -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', () => {

View file

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

View file

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

View file

@ -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: [],

View file

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

View file

@ -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: [],

View file

@ -754,6 +754,5 @@ function expectToMatchRuleSchema(obj: RuleResponse): void {
index: expect.arrayContaining([]),
query: expect.any(String),
actions: expect.arrayContaining([]),
investigation_fields: expect.arrayContaining([]),
});
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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': [],

View file

@ -102,5 +102,4 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial<RuleResponse> =
related_integrations: [],
required_fields: [],
setup: '',
investigation_fields: [],
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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