[8.11] [Security Solution][DE] Migrate investigation_fields (#169061) (#169957)

# Backport

This will backport the following commits from `main` to `8.11`:
- [[Security Solution][DE] Migrate investigation_fields
(#169061)](https://github.com/elastic/kibana/pull/169061)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Yara
Tercero","email":"yctercero@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-10-26T13:58:35Z","message":"[Security
Solution][DE] Migrate investigation_fields (#169061)\n\n##
Summary\r\n\r\n**TLDR:** SO will support both `string[]` and `{
field_names: string[]\r\n}`, but detection engine APIs will only support
the object format
in\r\n8.11+.","sha":"bb3673f2eb24013b11c736986928c3b73370f6bf","branchLabelMapping":{"^v8.12.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:breaking","Team:
SecuritySolution","Breaking Change","Team:Detection
Engine","v8.11.0","v8.12.0","v8.11.1"],"number":169061,"url":"https://github.com/elastic/kibana/pull/169061","mergeCommit":{"message":"[Security
Solution][DE] Migrate investigation_fields (#169061)\n\n##
Summary\r\n\r\n**TLDR:** SO will support both `string[]` and `{
field_names: string[]\r\n}`, but detection engine APIs will only support
the object format
in\r\n8.11+.","sha":"bb3673f2eb24013b11c736986928c3b73370f6bf"}},"sourceBranch":"main","suggestedTargetBranches":["8.11"],"targetPullRequestStates":[{"branch":"8.11","label":"v8.11.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.12.0","labelRegex":"^v8.12.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/169061","number":169061,"mergeCommit":{"message":"[Security
Solution][DE] Migrate investigation_fields (#169061)\n\n##
Summary\r\n\r\n**TLDR:** SO will support both `string[]` and `{
field_names: string[]\r\n}`, but detection engine APIs will only support
the object format
in\r\n8.11+.","sha":"bb3673f2eb24013b11c736986928c3b73370f6bf"}}]}]
BACKPORT-->

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Yara Tercero 2023-10-27 01:50:42 -07:00 committed by GitHub
parent 9829476388
commit b3385d532d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 2351 additions and 461 deletions

View file

@ -0,0 +1,122 @@
/*
* 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.
*/
import type { InvestigationFields } from '../../../../common/api/detection_engine';
import type { Rule } from './types';
import { transformRuleFromAlertHit } from './use_rule_with_fallback';
export const getMockAlertSearchResponse = (rule: Rule) => ({
took: 1,
timeout: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 75,
relation: 'eq',
},
max_score: null,
hits: [
{
_id: '1234',
_index: '.kibana',
_source: {
'@timestamp': '12334232132',
kibana: {
alert: {
rule,
},
},
},
},
],
},
});
describe('use_rule_with_fallback', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('transformRuleFromAlertHit', () => {
// Testing edge case, where if hook does not find the rule and turns to the alert document,
// the alert document could still have an unmigrated, legacy version of investigation_fields.
// We are not looking to do any migrations to these legacy fields in the alert document, so need
// to transform it on read in this case.
describe('investigation_fields', () => {
it('sets investigation_fields to undefined when set as legacy array', () => {
const mockRule = getMockRule({
investigation_fields: ['foo'] as unknown as InvestigationFields,
});
const mockHit = getMockAlertSearchResponse(mockRule);
const result = transformRuleFromAlertHit(mockHit);
expect(result?.investigation_fields).toBeUndefined();
});
it('sets investigation_fields to undefined when set as legacy empty array', () => {
// Ideally, we would have the client side types pull from the same types
// as server side so we could denote here that the SO can have investigation_fields
// as array or object, but our APIs now only support object. We don't have that here
// and would need to adjust the client side type to support both, which we do not want
// to do in this instance as we try to migrate folks away from the array version.
const mockRule = getMockRule({
investigation_fields: [] as unknown as InvestigationFields,
});
const mockHit = getMockAlertSearchResponse(mockRule);
const result = transformRuleFromAlertHit(mockHit);
expect(result?.investigation_fields).toBeUndefined();
});
it('does no transformation when "investigation_fields" is intended type', () => {
const mockRule = getMockRule({ investigation_fields: { field_names: ['bar'] } });
const mockHit = getMockAlertSearchResponse(mockRule);
const result = transformRuleFromAlertHit(mockHit);
expect(result?.investigation_fields).toEqual({ field_names: ['bar'] });
});
});
});
});
const getMockRule = (overwrites: Partial<Rule>): Rule => ({
id: 'myfakeruleid',
author: [],
severity_mapping: [],
risk_score_mapping: [],
rule_id: 'rule-1',
risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
name: 'some-name',
severity: 'low',
type: 'query',
query: 'some query',
index: ['index-1'],
interval: '5m',
references: [],
actions: [],
enabled: false,
false_positives: [],
max_signals: 100,
tags: [],
threat: [],
throttle: null,
version: 1,
exceptions_list: [],
created_at: '2020-04-09T09:43:51.778Z',
created_by: 'elastic',
immutable: false,
updated_at: '2020-04-09T09:43:51.778Z',
updated_by: 'elastic',
related_integrations: [],
required_fields: [],
setup: '',
...overwrites,
});

View file

@ -8,6 +8,8 @@
import { ALERT_RULE_UUID } from '@kbn/rule-data-utils';
import { isNotFoundError } from '@kbn/securitysolution-t-grid';
import { useEffect, useMemo } from 'react';
import type { InvestigationFieldsCombined } from '../../../../server/lib/detection_engine/rule_schema';
import type { InvestigationFields } from '../../../../common/api/detection_engine';
import { expandDottedObject } from '../../../../common/utils/expand_dotted';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
import { ALERTS_QUERY_NAMES } from '../../../detections/containers/detection_engine/alerts/constants';
@ -114,11 +116,50 @@ export const useRuleWithFallback = (ruleId: string): UseRuleWithFallback => {
};
};
/**
* In 8.10.x investigation_fields is mapped as alert, moving forward, it will be mapped
* as an object. This util is being used for the use case where a rule is deleted and the
* hook falls back to using the alert document to retrieve rule information. In this scenario
* we are going to return undefined if field is in legacy format to avoid any possible complexity
* in the UI for such flows. See PR 169061
* @param investigationFields InvestigationFieldsCombined | undefined
* @returns InvestigationFields | undefined
*/
export const migrateLegacyInvestigationFields = (
investigationFields: InvestigationFieldsCombined | undefined
): InvestigationFields | undefined => {
if (investigationFields && Array.isArray(investigationFields)) {
return undefined;
}
return investigationFields;
};
/**
* In 8.10.x investigation_fields is mapped as alert, moving forward, it will be mapped
* as an object. This util is being used for the use case where a rule is deleted and the
* hook falls back to using the alert document to retrieve rule information. In this scenario
* we are going to return undefined if field is in legacy format to avoid any possible complexity
* in the UI for such flows. See PR 169061
* @param rule Rule
* @returns Rule
*/
export const migrateRuleWithLegacyInvestigationFieldsFromAlertHit = (rule: Rule): Rule => {
if (!rule) return rule;
return {
...rule,
investigation_fields: migrateLegacyInvestigationFields(rule.investigation_fields),
};
};
/**
* Transforms an alertHit into a Rule
* @param data raw response containing single alert
*/
const transformRuleFromAlertHit = (data: AlertSearchResponse<AlertHit>): Rule | undefined => {
export const transformRuleFromAlertHit = (
data: AlertSearchResponse<AlertHit>
): Rule | undefined => {
// if results empty, return rule as undefined
if (data.hits.hits.length === 0) {
return undefined;
@ -136,8 +177,8 @@ const transformRuleFromAlertHit = (data: AlertSearchResponse<AlertHit>): Rule |
...expandedRuleWithParams?.kibana?.alert?.rule?.parameters,
};
delete expandedRule.parameters;
return expandedRule as Rule;
return migrateRuleWithLegacyInvestigationFieldsFromAlertHit(expandedRule as Rule);
}
return rule;
return migrateRuleWithLegacyInvestigationFieldsFromAlertHit(rule);
};

View file

@ -22,9 +22,7 @@ import {
findExceptionReferencesOnRuleSchema,
rulesReferencedByExceptionListsSchema,
} from '../../../../../../common/api/detection_engine/rule_exceptions';
import { enrichFilterWithRuleTypeMapping } from '../../../rule_management/logic/search/enrich_filter_with_rule_type_mappings';
import type { RuleParams } from '../../../rule_schema';
import { findRules } from '../../../rule_management/logic/search/find_rules';
export const findRuleExceptionReferencesRoute = (router: SecuritySolutionPluginRouter) => {
router.versioned
@ -92,15 +90,18 @@ export const findRuleExceptionReferencesRoute = (router: SecuritySolutionPluginR
}
const references: RuleReferencesSchema[] = await Promise.all(
foundExceptionLists.data.map(async (list, index) => {
const foundRules = await rulesClient.find<RuleParams>({
options: {
perPage: 10000,
filter: enrichFilterWithRuleTypeMapping(null),
hasReference: {
id: list.id,
type: getSavedObjectType({ namespaceType: list.namespace_type }),
},
const foundRules = await findRules({
rulesClient,
perPage: 10000,
hasReference: {
id: list.id,
type: getSavedObjectType({ namespaceType: list.namespace_type }),
},
filter: undefined,
fields: undefined,
sortField: undefined,
sortOrder: undefined,
page: undefined,
});
const ruleData = foundRules.data.map(({ name, id, params }) => ({

View file

@ -16,6 +16,7 @@ import {
getSampleDetailsAsNdjson,
} from '../../../../../../common/api/detection_engine/rule_management/mocks';
import type { RuleExceptionsPromiseFromStreams } from './import_rules_utils';
import type { InvestigationFields } from '../../../../../../common/api/detection_engine';
export const getOutputSample = (): Partial<RuleToImport> => ({
rule_id: 'rule-1',
@ -319,5 +320,62 @@ describe('create_rules_stream_from_ndjson', () => {
const resultOrError = result as BadRequestError[];
expect(resultOrError[1] instanceof BadRequestError).toEqual(true);
});
test('migrates investigation_fields', async () => {
const sample1 = {
...getOutputSample(),
investigation_fields: ['foo', 'bar'] as unknown as InvestigationFields,
};
const sample2 = {
...getOutputSample(),
rule_id: 'rule-2',
investigation_fields: [] as unknown as InvestigationFields,
};
sample2.rule_id = 'rule-2';
const ndJsonStream = new Readable({
read() {
this.push(getSampleAsNdjson(sample1));
this.push(getSampleAsNdjson(sample2));
this.push(null);
},
});
const rulesObjectsStream = createRulesAndExceptionsStreamFromNdJson(1000);
const [{ rules: result }] = await createPromiseFromStreams<
RuleExceptionsPromiseFromStreams[]
>([ndJsonStream, ...rulesObjectsStream]);
expect(result).toEqual([
{
rule_id: 'rule-1',
output_index: '.siem-signals',
risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
index: ['index-1'],
name: 'some-name',
severity: 'low',
interval: '5m',
type: 'query',
immutable: false,
investigation_fields: {
field_names: ['foo', 'bar'],
},
},
{
rule_id: 'rule-2',
output_index: '.siem-signals',
risk_score: 50,
description: 'some description',
from: 'now-5m',
to: 'now',
index: ['index-1'],
name: 'some-name',
severity: 'low',
interval: '5m',
type: 'query',
immutable: false,
},
]);
});
});
});

View file

@ -29,6 +29,7 @@ import {
RuleToImport,
validateRuleToImport,
} from '../../../../../../common/api/detection_engine/rule_management';
import type { RulesObjectsExportResultDetails } from '../../../../../utils/read_stream/create_stream_from_ndjson';
import {
parseNdjsonStrings,
createRulesLimitStream,
@ -103,6 +104,25 @@ export const sortImports = (): Transform => {
);
};
export const migrateLegacyInvestigationFields = (): Transform => {
return createMapStream<RuleToImport | RulesObjectsExportResultDetails>((obj) => {
if (obj != null && 'investigation_fields' in obj && Array.isArray(obj.investigation_fields)) {
if (obj.investigation_fields.length) {
return {
...obj,
investigation_fields: {
field_names: obj.investigation_fields,
},
};
} else {
const { investigation_fields: _, ...rest } = obj;
return rest;
}
}
return obj;
});
};
// TODO: Capture both the line number and the rule_id if you have that information for the error message
// eventually and then pass it down so we can give error messages on the line number
@ -111,6 +131,7 @@ export const createRulesAndExceptionsStreamFromNdJson = (ruleLimit: number) => {
createSplitStream('\n'),
parseNdjsonStrings(),
filterExportedCounts(),
migrateLegacyInvestigationFields(),
sortImports(),
validateRulesStream(),
createRulesLimitStream(ruleLimit),

View file

@ -5,7 +5,10 @@
* 2.0.
*/
import * as t from 'io-ts';
import type { FindResult, RulesClient } from '@kbn/alerting-plugin/server';
import { NonEmptyString, UUID } from '@kbn/securitysolution-io-ts-types';
import type { FindRulesSortFieldOrUndefined } from '../../../../../../common/api/detection_engine/rule_management';
import type {
@ -20,6 +23,15 @@ import type { RuleParams } from '../../../rule_schema';
import { enrichFilterWithRuleTypeMapping } from './enrich_filter_with_rule_type_mappings';
import { transformSortField } from './transform_sort_field';
type HasReferences = t.TypeOf<typeof HasReferences>;
const HasReferences = t.type({
type: NonEmptyString,
id: UUID,
});
type HasReferencesOrUndefined = t.TypeOf<typeof HasReferencesOrUndefined>;
const HasReferencesOrUndefined = t.union([HasReferences, t.undefined]);
export interface FindRuleOptions {
rulesClient: RulesClient;
filter: QueryFilterOrUndefined;
@ -28,6 +40,7 @@ export interface FindRuleOptions {
sortOrder: SortOrderOrUndefined;
page: PageOrUndefined;
perPage: PerPageOrUndefined;
hasReference?: HasReferencesOrUndefined;
}
export const findRules = ({
@ -38,6 +51,7 @@ export const findRules = ({
filter,
sortField,
sortOrder,
hasReference,
}: FindRuleOptions): Promise<FindResult<RuleParams>> => {
return rulesClient.find({
options: {
@ -47,6 +61,7 @@ export const findRules = ({
filter: enrichFilterWithRuleTypeMapping(filter),
sortOrder,
sortField: transformSortField(sortField),
hasReference,
},
});
};

View file

@ -77,7 +77,11 @@ import type {
NewTermsSpecificRuleParams,
} from '../../rule_schema';
import { transformFromAlertThrottle, transformToActionFrequency } from './rule_actions';
import { convertAlertSuppressionToCamel, convertAlertSuppressionToSnake } from '../utils/utils';
import {
convertAlertSuppressionToCamel,
convertAlertSuppressionToSnake,
migrateLegacyInvestigationFields,
} from '../utils/utils';
import { createRuleExecutionSummary } from '../../rule_monitoring';
import type { PrebuiltRuleAsset } from '../../prebuilt_rules';
@ -661,7 +665,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,
investigation_fields: migrateLegacyInvestigationFields(params.investigationFields),
author: params.author,
false_positives: params.falsePositives,
from: params.from,

View file

@ -26,6 +26,7 @@ import {
getInvalidConnectors,
swapActionIds,
migrateLegacyActionsIds,
migrateLegacyInvestigationFields,
} from './utils';
import { getRuleMock } from '../../routes/__mocks__/request_responses';
import type { PartialFilter } from '../../types';
@ -1259,4 +1260,26 @@ describe('utils', () => {
]);
});
});
describe('migrateLegacyInvestigationFields', () => {
test('should return undefined if value not set', () => {
const result = migrateLegacyInvestigationFields(undefined);
expect(result).toEqual(undefined);
});
test('should migrate array to object', () => {
const result = migrateLegacyInvestigationFields(['foo']);
expect(result).toEqual({ field_names: ['foo'] });
});
test('should migrate empty array to undefined', () => {
const result = migrateLegacyInvestigationFields([]);
expect(result).toEqual(undefined);
});
test('should not migrate if already intended type', () => {
const result = migrateLegacyInvestigationFields({ field_names: ['foo'] });
expect(result).toEqual({ field_names: ['foo'] });
});
});
});

View file

@ -20,11 +20,12 @@ import type {
} from '../../../../../common/api/detection_engine/rule_management';
import type {
AlertSuppression,
RuleResponse,
AlertSuppressionCamel,
InvestigationFields,
RuleResponse,
} from '../../../../../common/api/detection_engine/model/rule_schema';
import type { RuleAlertType, RuleParams } from '../../rule_schema';
import type { InvestigationFieldsCombined, RuleAlertType, RuleParams } from '../../rule_schema';
import { isAlertType } from '../../rule_schema';
import type { BulkError, OutputError } from '../../routes/utils';
import { createBulkErrorObject } from '../../routes/utils';
@ -380,3 +381,27 @@ export const convertAlertSuppressionToSnake = (
missing_fields_strategy: input.missingFieldsStrategy,
}
: undefined;
/**
* In ESS 8.10.x "investigation_fields" are mapped as string[].
* For 8.11+ logic is added on read in our endpoints to migrate
* the data over to it's intended type of { field_names: string[] }.
* The SO rule type will continue to support both types until we deprecate,
* but APIs will only support intended object format.
* See PR 169061
*/
export const migrateLegacyInvestigationFields = (
investigationFields: InvestigationFieldsCombined | undefined
): InvestigationFields | undefined => {
if (investigationFields && Array.isArray(investigationFields)) {
if (investigationFields.length) {
return {
field_names: investigationFields,
};
}
return undefined;
}
return investigationFields;
};

View file

@ -38,6 +38,7 @@ import {
} from '@kbn/securitysolution-rules';
import type { SanitizedRuleConfig } from '@kbn/alerting-plugin/common';
import { NonEmptyString } from '@kbn/securitysolution-io-ts-types';
import {
AlertsIndex,
AlertsIndexNamespace,
@ -58,7 +59,6 @@ import {
RuleAuthorArray,
RuleDescription,
RuleFalsePositiveArray,
InvestigationFields,
RuleFilterArray,
RuleLicense,
RuleMetadata,
@ -78,6 +78,7 @@ import {
TimestampField,
TimestampOverride,
TimestampOverrideFallbackDisabled,
InvestigationFields,
} from '../../../../../common/api/detection_engine/model/rule_schema';
import {
savedIdOrUndefined,
@ -87,6 +88,24 @@ import {
import { SERVER_APP_ID } from '../../../../../common/constants';
import { ResponseActionRuleParamsOrUndefined } from '../../../../../common/api/detection_engine/model/rule_response_actions';
// 8.10.x is mapped as an array of strings
export type LegacyInvestigationFields = t.TypeOf<typeof LegacyInvestigationFields>;
export const LegacyInvestigationFields = t.array(NonEmptyString);
/*
* In ESS 8.10.x "investigation_fields" are mapped as string[].
* For 8.11+ logic is added on read in our endpoints to migrate
* the data over to it's intended type of { field_names: string[] }.
* The SO rule type will continue to support both types until we deprecate,
* but APIs will only support intended object format.
* See PR 169061
*/
export type InvestigationFieldsCombined = t.TypeOf<typeof InvestigationFieldsCombined>;
export const InvestigationFieldsCombined = t.union([
InvestigationFields,
LegacyInvestigationFields,
]);
const nonEqlLanguages = t.keyof({ kuery: null, lucene: null });
export const baseRuleParams = t.exact(
@ -99,7 +118,7 @@ export const baseRuleParams = t.exact(
falsePositives: RuleFalsePositiveArray,
from: RuleIntervalFrom,
ruleId: RuleSignatureId,
investigationFields: t.union([InvestigationFields, t.undefined]),
investigationFields: t.union([InvestigationFieldsCombined, t.undefined]),
immutable: IsRuleImmutable,
license: t.union([RuleLicense, t.undefined]),
outputIndex: AlertsIndex,

View file

@ -6,7 +6,8 @@
*/
import expect from '@kbn/expect';
import { Rule } from '@kbn/alerting-plugin/common';
import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants';
import {
CreateExceptionListSchema,
@ -24,6 +25,9 @@ import {
deleteAllRules,
createExceptionList,
deleteAllAlerts,
getRuleSOById,
createRuleThroughAlertingEndpoint,
getRuleSavedObjectWithLegacyInvestigationFields,
} from '../../utils';
import {
deleteAllExceptions,
@ -246,5 +250,48 @@ export default ({ getService }: FtrProviderContext) => {
status_code: 500,
});
});
describe('legacy investigation_fields', () => {
let ruleWithLegacyInvestigationField: Rule<BaseRuleParams>;
beforeEach(async () => {
await deleteAllRules(supertest, log);
ruleWithLegacyInvestigationField = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFields()
);
});
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('creates and associates a `rule_default` exception list to a rule with a legacy investigation_field', async () => {
await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/${ruleWithLegacyInvestigationField.id}/exceptions`)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send({
items: [getRuleExceptionItemMock()],
})
.expect(200);
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change will
* NOT include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, ruleWithLegacyInvestigationField.id);
expect(
ruleSO?.alert.params.exceptionsList.some((list) => list.type === 'rule_default')
).to.eql(true);
expect(ruleSO?.alert.params.investigationFields).to.eql(['client.address', 'agent.name']);
});
});
});
};

View file

@ -526,6 +526,44 @@ export default ({ getService }: FtrProviderContext) => {
});
});
});
describe('investigation_fields', () => {
it('should create a rule with investigation_fields', async () => {
const rule = {
...getSimpleRule(),
investigation_fields: {
field_names: ['host.name'],
},
};
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send(rule)
.expect(200);
expect(body.investigation_fields).to.eql({
field_names: ['host.name'],
});
});
it('should NOT create a rule with legacy investigation_fields', async () => {
const rule = {
...getSimpleRule(),
investigation_fields: ['host.name'],
};
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send(rule)
.expect(400);
expect(body.message).to.eql(
'[request body]: Invalid value "["host.name"]" supplied to "investigation_fields"'
);
});
});
});
describe('missing timestamps', () => {

View file

@ -436,6 +436,21 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
});
describe('legacy investigation fields', () => {
it('should error trying to create a rule with legacy investigation fields format', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_CREATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([{ ...getSimpleRule(), investigation_fields: ['foo'] }])
.expect(400);
expect(body.message).to.eql(
'[request body]: Invalid value "["foo"]" supplied to "investigation_fields"'
);
});
});
});
});
};

View file

@ -6,7 +6,8 @@
*/
import expect from '@kbn/expect';
import { Rule } from '@kbn/alerting-plugin/common';
import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
import { BASE_ALERTING_API_PATH } from '@kbn/alerting-plugin/common';
import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@ -25,6 +26,9 @@ import {
removeServerGeneratedProperties,
removeServerGeneratedPropertiesIncludingRuleId,
getLegacyActionSO,
createRuleThroughAlertingEndpoint,
getRuleSavedObjectWithLegacyInvestigationFields,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
@ -239,5 +243,72 @@ export default ({ getService }: FtrProviderContext): void => {
expect(sidecarActionsPostResults.hits.hits.length).to.eql(0);
});
});
describe('legacy investigation fields', () => {
let ruleWithLegacyInvestigationField: Rule<BaseRuleParams>;
let ruleWithLegacyInvestigationFieldEmptyArray: Rule<BaseRuleParams>;
beforeEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
await createSignalsIndex(supertest, log);
ruleWithLegacyInvestigationField = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFields()
);
ruleWithLegacyInvestigationFieldEmptyArray = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray()
);
await createRule(supertest, log, {
...getSimpleRule('rule-with-investigation-field'),
name: 'Test investigation fields object',
investigation_fields: { field_names: ['host.name'] },
});
});
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('deletes rule with investigation fields as array', async () => {
const { body } = await supertest
.delete(
`${DETECTION_ENGINE_RULES_URL}?rule_id=${ruleWithLegacyInvestigationField.params.ruleId}`
)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare.investigation_fields).to.eql({
field_names: ['client.address', 'agent.name'],
});
});
it('deletes rule with investigation fields as empty array', async () => {
const { body } = await supertest
.delete(
`${DETECTION_ENGINE_RULES_URL}?rule_id=${ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId}`
)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare.investigation_fields).to.eql(undefined);
});
it('deletes rule with investigation fields as intended object type', async () => {
const { body } = await supertest
.delete(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-with-investigation-field`)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare.investigation_fields).to.eql({ field_names: ['host.name'] });
});
});
});
};

View file

@ -6,9 +6,11 @@
*/
import expect from '@kbn/expect';
import { Rule } from '@kbn/alerting-plugin/common';
import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
import { BASE_ALERTING_API_PATH } from '@kbn/alerting-plugin/common';
import { DETECTION_ENGINE_RULES_BULK_DELETE } from '@kbn/security-solution-plugin/common/constants';
import { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
createLegacyRuleAction,
@ -25,6 +27,9 @@ import {
removeServerGeneratedProperties,
removeServerGeneratedPropertiesIncludingRuleId,
getLegacyActionSO,
createRuleThroughAlertingEndpoint,
getRuleSavedObjectWithLegacyInvestigationFields,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
@ -444,5 +449,73 @@ export default ({ getService }: FtrProviderContext): void => {
expect(sidecarActionsPostResults.hits.hits.length).to.eql(0);
});
});
describe('legacy investigation fields', () => {
let ruleWithLegacyInvestigationField: Rule<BaseRuleParams>;
let ruleWithLegacyInvestigationFieldEmptyArray: Rule<BaseRuleParams>;
beforeEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
await createSignalsIndex(supertest, log);
ruleWithLegacyInvestigationField = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFields()
);
ruleWithLegacyInvestigationFieldEmptyArray = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray()
);
await createRule(supertest, log, {
...getSimpleRule('rule-with-investigation-field'),
name: 'Test investigation fields object',
investigation_fields: { field_names: ['host.name'] },
});
});
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('DELETE - should delete a single rule with investigation field', async () => {
// delete the rule in bulk
const { body } = await supertest
.delete(DETECTION_ENGINE_RULES_BULK_DELETE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([
{ rule_id: 'rule-with-investigation-field' },
{ rule_id: ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId },
{ rule_id: ruleWithLegacyInvestigationField.params.ruleId },
])
.expect(200);
const investigationFields = body.map((rule: RuleResponse) => rule.investigation_fields);
expect(investigationFields).to.eql([
{ field_names: ['host.name'] },
undefined,
{ field_names: ['client.address', 'agent.name'] },
]);
});
it('POST - should delete a single rule with investigation field', async () => {
// delete the rule in bulk
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_DELETE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([
{ rule_id: 'rule-with-investigation-field' },
{ rule_id: ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId },
{ rule_id: ruleWithLegacyInvestigationField.params.ruleId },
])
.expect(200);
const investigationFields = body.map((rule: RuleResponse) => rule.investigation_fields);
expect(investigationFields).to.eql([
{ field_names: ['host.name'] },
undefined,
{ field_names: ['client.address', 'agent.name'] },
]);
});
});
});
};

View file

@ -7,6 +7,9 @@
import expect from 'expect';
import { Rule } from '@kbn/alerting-plugin/common';
import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
import {
DETECTION_ENGINE_RULES_URL,
UPDATE_OR_CREATE_LEGACY_ACTIONS,
@ -24,6 +27,10 @@ import {
getWebHookAction,
removeServerGeneratedProperties,
waitForRulePartialFailure,
getRuleSavedObjectWithLegacyInvestigationFields,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
getRuleSOById,
createRuleThroughAlertingEndpoint,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
@ -720,6 +727,123 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
});
describe('legacy investigation fields', () => {
let ruleWithLegacyInvestigationField: Rule<BaseRuleParams>;
let ruleWithLegacyInvestigationFieldEmptyArray: Rule<BaseRuleParams>;
beforeEach(async () => {
await deleteAllRules(supertest, log);
ruleWithLegacyInvestigationField = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFields()
);
ruleWithLegacyInvestigationFieldEmptyArray = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray()
);
await createRule(supertest, log, {
...getSimpleRule('rule-with-investigation-field'),
name: 'Test investigation fields object',
investigation_fields: { field_names: ['host.name'] },
});
});
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('exports a rule that has legacy investigation_field and transforms field in response', async () => {
const { body } = await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_export`)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send({
objects: [{ rule_id: ruleWithLegacyInvestigationField.params.ruleId }],
})
.expect(200)
.parse(binaryToString);
const exportedRule = JSON.parse(body.toString().split(/\n/)[0]);
expect(exportedRule.investigation_fields).toEqual({
field_names: ['client.address', 'agent.name'],
});
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should
* NOT include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, ruleWithLegacyInvestigationField.id);
expect(ruleSO?.alert?.params?.investigationFields).toEqual([
'client.address',
'agent.name',
]);
});
it('exports a rule that has a legacy investigation field set to empty array and unsets field in response', async () => {
const { body } = await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_export`)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send({
objects: [{ rule_id: ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId }],
})
.expect(200)
.parse(binaryToString);
const exportedRule = JSON.parse(body.toString().split(/\n/)[0]);
expect(exportedRule.investigation_fields).toEqual(undefined);
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should
* NOT include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, ruleWithLegacyInvestigationFieldEmptyArray.id);
expect(ruleSO?.alert?.params?.investigationFields).toEqual([]);
});
it('exports rule with investigation fields as intended object type', async () => {
const { body } = await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_export`)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send({
objects: [{ rule_id: 'rule-with-investigation-field' }],
})
.expect(200)
.parse(binaryToString);
const exportedRule = JSON.parse(body.toString().split(/\n/)[0]);
expect(exportedRule.investigation_fields).toEqual({
field_names: ['host.name'],
});
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should
* NOT include a migration on SO.
*/ const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, exportedRule.id);
expect(ruleSO?.alert?.params?.investigationFields).toEqual({ field_names: ['host.name'] });
});
});
});
};

View file

@ -6,7 +6,9 @@
*/
import expect from '@kbn/expect';
import { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { Rule } from '@kbn/alerting-plugin/common';
import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
import {
DETECTION_ENGINE_RULES_URL,
UPDATE_OR_CREATE_LEGACY_ACTIONS,
@ -14,19 +16,24 @@ import {
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
createRule,
createRuleThroughAlertingEndpoint,
deleteAllRules,
getComplexRule,
getComplexRuleOutput,
getRuleSOById,
getSimpleRule,
getSimpleRuleOutput,
getWebHookAction,
removeServerGeneratedProperties,
getRuleSavedObjectWithLegacyInvestigationFields,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest');
const log = getService('log');
const es = getService('es');
describe('find_rules', () => {
beforeEach(async () => {
@ -267,5 +274,82 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
});
describe('legacy investigation fields', () => {
let ruleWithLegacyInvestigationField: Rule<BaseRuleParams>;
let ruleWithLegacyInvestigationFieldEmptyArray: Rule<BaseRuleParams>;
beforeEach(async () => {
await deleteAllRules(supertest, log);
ruleWithLegacyInvestigationField = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFields()
);
ruleWithLegacyInvestigationFieldEmptyArray = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray()
);
await createRule(supertest, log, {
...getSimpleRule('rule-with-investigation-field'),
name: 'Test investigation fields object',
investigation_fields: { field_names: ['host.name'] },
});
});
it('should return a rule with the migrated investigation fields', async () => {
const { body } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}/_find`)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send()
.expect(200);
const [ruleWithFieldAsArray] = body.data.filter(
(rule: RuleResponse) => rule.rule_id === ruleWithLegacyInvestigationField.params.ruleId
);
const [ruleWithFieldAsEmptyArray] = body.data.filter(
(rule: RuleResponse) =>
rule.rule_id === ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId
);
const [ruleWithExpectedTyping] = body.data.filter(
(rule: RuleResponse) => rule.rule_id === 'rule-with-investigation-field'
);
expect(ruleWithFieldAsArray.investigation_fields).to.eql({
field_names: ['client.address', 'agent.name'],
});
expect(ruleWithFieldAsEmptyArray.investigation_fields).to.eql(undefined);
expect(ruleWithExpectedTyping.investigation_fields).to.eql({
field_names: ['host.name'],
});
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should
* NOT include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, ruleWithLegacyInvestigationField.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql(['client.address', 'agent.name']);
const {
hits: {
hits: [{ _source: ruleSO2 }],
},
} = await getRuleSOById(es, ruleWithLegacyInvestigationFieldEmptyArray.id);
expect(ruleSO2?.alert?.params?.investigationFields).to.eql([]);
const {
hits: {
hits: [{ _source: ruleSO3 }],
},
} = await getRuleSOById(es, ruleWithExpectedTyping.id);
expect(ruleSO3?.alert?.params?.investigationFields).to.eql({ field_names: ['host.name'] });
});
});
});
};

View file

@ -7,7 +7,11 @@
import expect from '@kbn/expect';
import { RuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine';
import {
InvestigationFields,
QueryRuleCreateProps,
RuleCreateProps,
} from '@kbn/security-solution-plugin/common/api/detection_engine';
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
import { getCreateExceptionListMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock';
import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants';
@ -34,6 +38,8 @@ import {
createLegacyRuleAction,
getLegacyActionSO,
createRule,
getRule,
getRuleSOById,
} from '../../utils';
import { deleteAllExceptions } from '../../../lists_api_integration/utils';
import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution';
@ -177,6 +183,18 @@ const getImportRuleWithConnectorsBuffer = (connectorId: string) => {
return buffer;
};
export const getSimpleRuleAsNdjsonWithLegacyInvestigationField = (
ruleIds: string[],
enabled = false,
overwrites: Partial<QueryRuleCreateProps>
): Buffer => {
const stringOfRules = ruleIds.map((ruleId) => {
const simpleRule = { ...getSimpleRule(ruleId, enabled), ...overwrites };
return JSON.stringify(simpleRule);
});
return Buffer.from(stringOfRules.join('\n'));
};
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest');
@ -1885,5 +1903,119 @@ export default ({ getService }: FtrProviderContext): void => {
.expect(200);
});
});
describe('legacy investigation fields', () => {
beforeEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
await createSignalsIndex(supertest, log);
});
afterEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
});
it('imports rule with investigation fields as array', async () => {
await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.attach(
'file',
getSimpleRuleAsNdjsonWithLegacyInvestigationField(['rule-1'], false, {
// mimicking what an 8.10 rule would look like
// we don't want to support this type in our APIs any longer, but do
// want to allow users to import rules from 8.10
investigation_fields: ['foo', 'bar'] as unknown as InvestigationFields,
}),
'rules.ndjson'
)
.expect('Content-Type', 'application/json; charset=utf-8')
.expect(200);
const rule = await getRule(supertest, log, 'rule-1');
expect(rule.investigation_fields).to.eql({ field_names: ['foo', 'bar'] });
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should
* include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, rule.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql({ field_names: ['foo', 'bar'] });
});
it('imports rule with investigation fields as empty array', async () => {
await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.attach(
'file',
getSimpleRuleAsNdjsonWithLegacyInvestigationField(['rule-1'], false, {
// mimicking what an 8.10 rule would look like
// we don't want to support this type in our APIs any longer, but do
// want to allow users to import rules from 8.10
investigation_fields: [] as unknown as InvestigationFields,
}),
'rules.ndjson'
)
.expect('Content-Type', 'application/json; charset=utf-8')
.expect(200);
const rule = await getRule(supertest, log, 'rule-1');
expect(rule.investigation_fields).to.eql(undefined);
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should
* include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, rule.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql(undefined);
});
it('imports rule with investigation fields as intended object type', async () => {
await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.attach(
'file',
getSimpleRuleAsNdjsonWithLegacyInvestigationField(['rule-1'], false, {
investigation_fields: {
field_names: ['foo'],
},
}),
'rules.ndjson'
)
.expect('Content-Type', 'application/json; charset=utf-8')
.expect(200);
const rule = await getRule(supertest, log, 'rule-1');
expect(rule.investigation_fields).to.eql({ field_names: ['foo'] });
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should
* include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, rule.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql({ field_names: ['foo'] });
});
});
});
};

View file

@ -6,6 +6,8 @@
*/
import expect from '@kbn/expect';
import { Rule } from '@kbn/alerting-plugin/common';
import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
import {
DETECTION_ENGINE_RULES_URL,
@ -31,6 +33,10 @@ import {
createLegacyRuleAction,
getLegacyActionSO,
getSimpleRuleWithoutRuleId,
getRuleSOById,
createRuleThroughAlertingEndpoint,
getRuleSavedObjectWithLegacyInvestigationFields,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
} from '../../utils';
import {
getActionsWithFrequencies,
@ -444,7 +450,182 @@ export default ({ getService }: FtrProviderContext) => {
});
});
describe('investigation_fields', () => {
describe('patch per-action frequencies', () => {
const patchSingleRule = async (
ruleId: string,
throttle: RuleActionThrottle | undefined,
actions: RuleActionArray
) => {
const { body: patchedRule } = await supertest
.patch(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send({ rule_id: ruleId, throttle, actions })
.expect(200);
patchedRule.actions = removeUUIDFromActions(patchedRule.actions);
return removeServerGeneratedPropertiesIncludingRuleId(patchedRule);
};
describe('actions without frequencies', () => {
[undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach(
(throttle) => {
it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => {
const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest);
// create simple rule
const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
// patch a simple rule's `throttle` and `actions`
const patchedRule = await patchSingleRule(
createdRule.rule_id,
throttle,
actionsWithoutFrequencies
);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.revision = 1;
expectedRule.actions = actionsWithoutFrequencies.map((action) => ({
...action,
frequency: NOTIFICATION_DEFAULT_FREQUENCY,
}));
expect(patchedRule).to.eql(expectedRule);
});
}
);
// Action throttle cannot be shorter than the schedule interval which is by default is 5m
['300s', '5m', '3h', '4d'].forEach((throttle) => {
it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => {
const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest);
// create simple rule
const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
// patch a simple rule's `throttle` and `actions`
const patchedRule = await patchSingleRule(
createdRule.rule_id,
throttle,
actionsWithoutFrequencies
);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.revision = 1;
expectedRule.actions = actionsWithoutFrequencies.map((action) => ({
...action,
frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' },
}));
expect(patchedRule).to.eql(expectedRule);
});
});
});
describe('actions with frequencies', () => {
[
undefined,
NOTIFICATION_THROTTLE_NO_ACTIONS,
NOTIFICATION_THROTTLE_RULE,
'321s',
'6m',
'10h',
'2d',
].forEach((throttle) => {
it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => {
const actionsWithFrequencies = await getActionsWithFrequencies(supertest);
// create simple rule
const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
// patch a simple rule's `throttle` and `actions`
const patchedRule = await patchSingleRule(
createdRule.rule_id,
throttle,
actionsWithFrequencies
);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.revision = 1;
expectedRule.actions = actionsWithFrequencies;
expect(patchedRule).to.eql(expectedRule);
});
});
});
describe('some actions with frequencies', () => {
[undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach(
(throttle) => {
it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => {
const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest);
// create simple rule
const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
// patch a simple rule's `throttle` and `actions`
const patchedRule = await patchSingleRule(
createdRule.rule_id,
throttle,
someActionsWithFrequencies
);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.revision = 1;
expectedRule.actions = someActionsWithFrequencies.map((action) => ({
...action,
frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY,
}));
expect(patchedRule).to.eql(expectedRule);
});
}
);
// Action throttle cannot be shorter than the schedule interval which is by default is 5m
['430s', '7m', '1h', '8d'].forEach((throttle) => {
it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => {
const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest);
// create simple rule
const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
// patch a simple rule's `throttle` and `actions`
const patchedRule = await patchSingleRule(
createdRule.rule_id,
throttle,
someActionsWithFrequencies
);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.revision = 1;
expectedRule.actions = someActionsWithFrequencies.map((action) => ({
...action,
frequency: action.frequency ?? {
summary: true,
throttle,
notifyWhen: 'onThrottleInterval',
},
}));
expect(patchedRule).to.eql(expectedRule);
});
});
});
});
});
describe('investigation fields', () => {
describe('investigation_field', () => {
beforeEach(async () => {
await createSignalsIndex(supertest, log);
});
afterEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
});
it('should overwrite investigation_fields value on patch - non additive', async () => {
await createRule(supertest, log, {
...getSimpleRule('rule-1'),
@ -506,168 +687,100 @@ export default ({ getService }: FtrProviderContext) => {
expect(body.investigation_fields.field_names).to.eql(['blob', 'boop']);
});
});
});
describe('patch per-action frequencies', () => {
const patchSingleRule = async (
ruleId: string,
throttle: RuleActionThrottle | undefined,
actions: RuleActionArray
) => {
const { body: patchedRule } = await supertest
.patch(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send({ rule_id: ruleId, throttle, actions })
.expect(200);
describe('investigation_fields legacy', () => {
let ruleWithLegacyInvestigationField: Rule<BaseRuleParams>;
let ruleWithLegacyInvestigationFieldEmptyArray: Rule<BaseRuleParams>;
patchedRule.actions = removeUUIDFromActions(patchedRule.actions);
return removeServerGeneratedPropertiesIncludingRuleId(patchedRule);
};
describe('actions without frequencies', () => {
[undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach(
(throttle) => {
it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => {
const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest);
// create simple rule
const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
// patch a simple rule's `throttle` and `actions`
const patchedRule = await patchSingleRule(
createdRule.rule_id,
throttle,
actionsWithoutFrequencies
);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.revision = 1;
expectedRule.actions = actionsWithoutFrequencies.map((action) => ({
...action,
frequency: NOTIFICATION_DEFAULT_FREQUENCY,
}));
expect(patchedRule).to.eql(expectedRule);
});
}
);
// Action throttle cannot be shorter than the schedule interval which is by default is 5m
['300s', '5m', '3h', '4d'].forEach((throttle) => {
it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => {
const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest);
// create simple rule
const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
// patch a simple rule's `throttle` and `actions`
const patchedRule = await patchSingleRule(
createdRule.rule_id,
throttle,
actionsWithoutFrequencies
);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.revision = 1;
expectedRule.actions = actionsWithoutFrequencies.map((action) => ({
...action,
frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' },
}));
expect(patchedRule).to.eql(expectedRule);
});
beforeEach(async () => {
ruleWithLegacyInvestigationField = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFields()
);
ruleWithLegacyInvestigationFieldEmptyArray = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray()
);
});
});
describe('actions with frequencies', () => {
[
undefined,
NOTIFICATION_THROTTLE_NO_ACTIONS,
NOTIFICATION_THROTTLE_RULE,
'321s',
'6m',
'10h',
'2d',
].forEach((throttle) => {
it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => {
const actionsWithFrequencies = await getActionsWithFrequencies(supertest);
// create simple rule
const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
// patch a simple rule's `throttle` and `actions`
const patchedRule = await patchSingleRule(
createdRule.rule_id,
throttle,
actionsWithFrequencies
);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.revision = 1;
expectedRule.actions = actionsWithFrequencies;
expect(patchedRule).to.eql(expectedRule);
});
afterEach(async () => {
await deleteAllRules(supertest, log);
});
});
describe('some actions with frequencies', () => {
[undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach(
(throttle) => {
it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => {
const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest);
it('errors if trying to patch investigation fields using legacy format', async () => {
const { body } = await supertest
.patch(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send({
rule_id: ruleWithLegacyInvestigationField.params.ruleId,
name: 'some other name',
investigation_fields: ['client.foo'],
})
.expect(400);
// create simple rule
const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
expect(body.message).to.eql(
'[request body]: Invalid value "["client.foo"]" supplied to "investigation_fields"'
);
});
// patch a simple rule's `throttle` and `actions`
const patchedRule = await patchSingleRule(
createdRule.rule_id,
throttle,
someActionsWithFrequencies
);
it('should patch a rule with a legacy investigation field and transform response', async () => {
const { body } = await supertest
.patch(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send({
rule_id: ruleWithLegacyInvestigationField.params.ruleId,
name: 'some other name',
})
.expect(200);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.revision = 1;
expectedRule.actions = someActionsWithFrequencies.map((action) => ({
...action,
frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY,
}));
expect(patchedRule).to.eql(expectedRule);
});
}
);
// Action throttle cannot be shorter than the schedule interval which is by default is 5m
['430s', '7m', '1h', '8d'].forEach((throttle) => {
it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => {
const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest);
// create simple rule
const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
// patch a simple rule's `throttle` and `actions`
const patchedRule = await patchSingleRule(
createdRule.rule_id,
throttle,
someActionsWithFrequencies
);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.revision = 1;
expectedRule.actions = someActionsWithFrequencies.map((action) => ({
...action,
frequency: action.frequency ?? {
summary: true,
throttle,
notifyWhen: 'onThrottleInterval',
},
}));
expect(patchedRule).to.eql(expectedRule);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare.investigation_fields).to.eql({
field_names: ['client.address', 'agent.name'],
});
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should
* NOT include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, body.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql([
'client.address',
'agent.name',
]);
});
it('should patch a rule with a legacy investigation field - empty array - and transform response', async () => {
const { body } = await supertest
.patch(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send({
rule_id: ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId,
name: 'some other name',
})
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare.investigation_fields).to.eql(undefined);
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should
* NOT include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, body.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql([]);
});
});
});

View file

@ -9,6 +9,8 @@ import expect from '@kbn/expect';
import { DETECTION_ENGINE_RULES_BULK_UPDATE } from '@kbn/security-solution-plugin/common/constants';
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { Rule } from '@kbn/alerting-plugin/common';
import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
@ -23,6 +25,10 @@ import {
createRule,
createLegacyRuleAction,
getLegacyActionSO,
getRuleSOById,
createRuleThroughAlertingEndpoint,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
getRuleSavedObjectWithLegacyInvestigationFields,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
@ -499,5 +505,144 @@ export default ({ getService }: FtrProviderContext) => {
]);
});
});
describe('legacy investigation fields', () => {
let ruleWithLegacyInvestigationField: Rule<BaseRuleParams>;
let ruleWithLegacyInvestigationFieldEmptyArray: Rule<BaseRuleParams>;
beforeEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
await createSignalsIndex(supertest, log);
ruleWithLegacyInvestigationField = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFields()
);
ruleWithLegacyInvestigationFieldEmptyArray = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray()
);
});
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('errors if trying to patch investigation fields using legacy format', async () => {
const { body } = await supertest
.patch(DETECTION_ENGINE_RULES_BULK_UPDATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([
{
rule_id: ruleWithLegacyInvestigationField.params.ruleId,
name: 'some other name',
investigation_fields: ['foobar'],
},
])
.expect(400);
expect(body.message).to.eql(
'[request body]: Invalid value "["foobar"]" supplied to "investigation_fields"'
);
});
it('should patch a rule with a legacy investigation field and transform field in response', async () => {
// patch a simple rule's name
const { body } = await supertest
.patch(DETECTION_ENGINE_RULES_BULK_UPDATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([
{ rule_id: ruleWithLegacyInvestigationField.params.ruleId, name: 'some other name' },
])
.expect(200);
const bodyToCompareLegacyField = removeServerGeneratedProperties(body[0]);
expect(bodyToCompareLegacyField.investigation_fields).to.eql({
field_names: ['client.address', 'agent.name'],
});
expect(bodyToCompareLegacyField.name).to.eql('some other name');
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should
* NOT include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, body[0].id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql(['client.address', 'agent.name']);
});
it('should patch a rule with a legacy investigation field - empty array - and transform field in response', async () => {
// patch a simple rule's name
const { body } = await supertest
.patch(DETECTION_ENGINE_RULES_BULK_UPDATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([
{
rule_id: ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId,
name: 'some other name 2',
},
])
.expect(200);
const bodyToCompareLegacyFieldEmptyArray = removeServerGeneratedProperties(body[0]);
expect(bodyToCompareLegacyFieldEmptyArray.investigation_fields).to.eql(undefined);
expect(bodyToCompareLegacyFieldEmptyArray.name).to.eql('some other name 2');
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should
* NOT include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, body[0].id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql([]);
});
it('should patch a rule with an investigation field', async () => {
await createRule(supertest, log, {
...getSimpleRule('rule-1'),
investigation_fields: {
field_names: ['host.name'],
},
});
// patch a simple rule's name
const { body } = await supertest
.patch(DETECTION_ENGINE_RULES_BULK_UPDATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([
{
rule_id: 'rule-1',
name: 'some other name 3',
},
])
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare.investigation_fields).to.eql({
field_names: ['host.name'],
});
expect(bodyToCompare.name).to.eql('some other name 3');
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, body[0].id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql({
field_names: ['host.name'],
});
});
});
});
};

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { Rule } from '@kbn/alerting-plugin/common';
import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
import expect from '@kbn/expect';
import { getCreateEsqlRulesSchemaMock } from '@kbn/security-solution-plugin/common/api/detection_engine/model/rule_schema/mocks';
import {
@ -38,6 +40,10 @@ import {
installMockPrebuiltRules,
removeServerGeneratedProperties,
waitForRuleSuccess,
getRuleSOById,
createRuleThroughAlertingEndpoint,
getRuleSavedObjectWithLegacyInvestigationFields,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
} from '../../utils';
import { FtrProviderContext } from '../../common/ftr_provider_context';
@ -2997,5 +3003,421 @@ export default ({ getService }: FtrProviderContext): void => {
expect(rule.timeline_id).to.eql(timelineId);
expect(rule.timeline_title).to.eql(timelineTitle);
});
describe('legacy investigation fields', () => {
let ruleWithLegacyInvestigationField: Rule<BaseRuleParams>;
let ruleWithLegacyInvestigationFieldEmptyArray: Rule<BaseRuleParams>;
beforeEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
await createSignalsIndex(supertest, log);
ruleWithLegacyInvestigationField = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFields()
);
ruleWithLegacyInvestigationFieldEmptyArray = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray()
);
await createRule(supertest, log, {
...getSimpleRule('rule-with-investigation-field'),
name: 'Test investigation fields object',
investigation_fields: { field_names: ['host.name'] },
});
});
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('should export rules with legacy investigation_fields and transform legacy field in response', async () => {
const { body } = await postBulkAction()
.send({ query: '', action: BulkActionType.export })
.expect(200)
.expect('Content-Type', 'application/ndjson')
.expect('Content-Disposition', 'attachment; filename="rules_export.ndjson"')
.parse(binaryToString);
const [rule1, rule2, rule3, exportDetailsJson] = body.toString().split(/\n/);
const ruleToCompareWithLegacyInvestigationField = removeServerGeneratedProperties(
JSON.parse(rule1)
);
expect(ruleToCompareWithLegacyInvestigationField.investigation_fields).to.eql({
field_names: ['client.address', 'agent.name'],
});
const ruleToCompareWithLegacyInvestigationFieldEmptyArray = removeServerGeneratedProperties(
JSON.parse(rule2)
);
expect(ruleToCompareWithLegacyInvestigationFieldEmptyArray.investigation_fields).to.eql(
undefined
);
const ruleWithInvestigationField = removeServerGeneratedProperties(JSON.parse(rule3));
expect(ruleWithInvestigationField.investigation_fields).to.eql({
field_names: ['host.name'],
});
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should not include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, JSON.parse(rule1).id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql(['client.address', 'agent.name']);
const exportDetails = JSON.parse(exportDetailsJson);
expect(exportDetails).to.eql({
exported_exception_list_count: 0,
exported_exception_list_item_count: 0,
exported_count: 3,
exported_rules_count: 3,
missing_exception_list_item_count: 0,
missing_exception_list_items: [],
missing_exception_lists: [],
missing_exception_lists_count: 0,
missing_rules: [],
missing_rules_count: 0,
excluded_action_connection_count: 0,
excluded_action_connections: [],
exported_action_connector_count: 0,
missing_action_connection_count: 0,
missing_action_connections: [],
});
});
it('should delete rules with investigation fields and transform legacy field in response', async () => {
const { body } = await postBulkAction()
.send({ query: '', action: BulkActionType.delete })
.expect(200);
expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 3, total: 3 });
// Check that the deleted rule is returned with the response
const names = body.attributes.results.deleted.map(
(returnedRule: RuleResponse) => returnedRule.name
);
expect(names.includes('Test investigation fields')).to.eql(true);
expect(names.includes('Test investigation fields empty array')).to.eql(true);
expect(names.includes('Test investigation fields object')).to.eql(true);
const ruleWithLegacyField = body.attributes.results.deleted.find(
(returnedRule: RuleResponse) =>
returnedRule.rule_id === ruleWithLegacyInvestigationField.params.ruleId
);
expect(ruleWithLegacyField.investigation_fields).to.eql({
field_names: ['client.address', 'agent.name'],
});
// Check that the updates have been persisted
await fetchRule(ruleWithLegacyInvestigationField.params.ruleId).expect(404);
await fetchRule(ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId).expect(404);
await fetchRule('rule-with-investigation-field').expect(404);
});
it('should enable rules with legacy investigation fields and transform legacy field in response', async () => {
const { body } = await postBulkAction()
.send({ query: '', action: BulkActionType.enable })
.expect(200);
expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 3, total: 3 });
// Check that the updated rule is returned with the response
// and field transformed on response
expect(
body.attributes.results.updated.every(
(returnedRule: RuleResponse) => returnedRule.enabled
)
).to.eql(true);
const ruleWithLegacyField = body.attributes.results.updated.find(
(returnedRule: RuleResponse) =>
returnedRule.rule_id === ruleWithLegacyInvestigationField.params.ruleId
);
expect(ruleWithLegacyField.investigation_fields).to.eql({
field_names: ['client.address', 'agent.name'],
});
const ruleWithEmptyArray = body.attributes.results.updated.find(
(returnedRule: RuleResponse) =>
returnedRule.rule_id === ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId
);
expect(ruleWithEmptyArray.investigation_fields).to.eql(undefined);
const ruleWithIntendedType = body.attributes.results.updated.find(
(returnedRule: RuleResponse) => returnedRule.rule_id === 'rule-with-investigation-field'
);
expect(ruleWithIntendedType.investigation_fields).to.eql({ field_names: ['host.name'] });
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should not include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, ruleWithLegacyField.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql(['client.address', 'agent.name']);
expect(ruleSO?.alert?.enabled).to.eql(true);
const {
hits: {
hits: [{ _source: ruleSO2 }],
},
} = await getRuleSOById(es, ruleWithEmptyArray.id);
expect(ruleSO2?.alert?.params?.investigationFields).to.eql([]);
expect(ruleSO?.alert?.enabled).to.eql(true);
const {
hits: {
hits: [{ _source: ruleSO3 }],
},
} = await getRuleSOById(es, ruleWithIntendedType.id);
expect(ruleSO3?.alert?.params?.investigationFields).to.eql({ field_names: ['host.name'] });
expect(ruleSO?.alert?.enabled).to.eql(true);
});
it('should disable rules with legacy investigation fields and transform legacy field in response', async () => {
const { body } = await postBulkAction()
.send({ query: '', action: BulkActionType.disable })
.expect(200);
expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 3, total: 3 });
// Check that the updated rule is returned with the response
// and field transformed on response
expect(
body.attributes.results.updated.every(
(returnedRule: RuleResponse) => !returnedRule.enabled
)
).to.eql(true);
const ruleWithLegacyField = body.attributes.results.updated.find(
(returnedRule: RuleResponse) =>
returnedRule.rule_id === ruleWithLegacyInvestigationField.params.ruleId
);
expect(ruleWithLegacyField.investigation_fields).to.eql({
field_names: ['client.address', 'agent.name'],
});
const ruleWithEmptyArray = body.attributes.results.updated.find(
(returnedRule: RuleResponse) =>
returnedRule.rule_id === ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId
);
expect(ruleWithEmptyArray.investigation_fields).to.eql(undefined);
const ruleWithIntendedType = body.attributes.results.updated.find(
(returnedRule: RuleResponse) => returnedRule.rule_id === 'rule-with-investigation-field'
);
expect(ruleWithIntendedType.investigation_fields).to.eql({ field_names: ['host.name'] });
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should not include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, ruleWithLegacyField.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql(['client.address', 'agent.name']);
const {
hits: {
hits: [{ _source: ruleSO2 }],
},
} = await getRuleSOById(es, ruleWithEmptyArray.id);
expect(ruleSO2?.alert?.params?.investigationFields).to.eql([]);
const {
hits: {
hits: [{ _source: ruleSO3 }],
},
} = await getRuleSOById(es, ruleWithIntendedType.id);
expect(ruleSO3?.alert?.params?.investigationFields).to.eql({ field_names: ['host.name'] });
});
it('should duplicate rules with legacy investigation fields and transform field in response', async () => {
const { body } = await postBulkAction()
.send({
query: '',
action: BulkActionType.duplicate,
duplicate: { include_exceptions: false, include_expired_exceptions: false },
})
.expect(200);
expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 3, total: 3 });
// Check that the duplicated rule is returned with the response
const names = body.attributes.results.created.map(
(returnedRule: RuleResponse) => returnedRule.name
);
expect(names.includes('Test investigation fields [Duplicate]')).to.eql(true);
expect(names.includes('Test investigation fields empty array [Duplicate]')).to.eql(true);
expect(names.includes('Test investigation fields object [Duplicate]')).to.eql(true);
// Check that the updates have been persisted
const { body: rulesResponse } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}/_find`)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.expect(200);
expect(rulesResponse.total).to.eql(6);
const ruleWithLegacyField = body.attributes.results.created.find(
(returnedRule: RuleResponse) =>
returnedRule.name === 'Test investigation fields [Duplicate]'
);
const ruleWithEmptyArray = body.attributes.results.created.find(
(returnedRule: RuleResponse) =>
returnedRule.name === 'Test investigation fields empty array [Duplicate]'
);
const ruleWithIntendedType = body.attributes.results.created.find(
(returnedRule: RuleResponse) =>
returnedRule.name === 'Test investigation fields object [Duplicate]'
);
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, duplicated
* rules should NOT have migrated value on write.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, ruleWithLegacyField.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql(['client.address', 'agent.name']);
const {
hits: {
hits: [{ _source: ruleSO2 }],
},
} = await getRuleSOById(es, ruleWithEmptyArray.id);
expect(ruleSO2?.alert?.params?.investigationFields).to.eql([]);
const {
hits: {
hits: [{ _source: ruleSO3 }],
},
} = await getRuleSOById(es, ruleWithIntendedType.id);
expect(ruleSO3?.alert?.params?.investigationFields).to.eql({ field_names: ['host.name'] });
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, the original
* rules selected to be duplicated should not be migrated.
*/
const {
hits: {
hits: [{ _source: ruleSOOriginalLegacy }],
},
} = await getRuleSOById(es, ruleWithLegacyInvestigationField.id);
expect(ruleSOOriginalLegacy?.alert?.params?.investigationFields).to.eql([
'client.address',
'agent.name',
]);
const {
hits: {
hits: [{ _source: ruleSOOriginalLegacyEmptyArray }],
},
} = await getRuleSOById(es, ruleWithLegacyInvestigationFieldEmptyArray.id);
expect(ruleSOOriginalLegacyEmptyArray?.alert?.params?.investigationFields).to.eql([]);
const {
hits: {
hits: [{ _source: ruleSOOriginalNoLegacy }],
},
} = await getRuleSOById(es, ruleWithIntendedType.id);
expect(ruleSOOriginalNoLegacy?.alert?.params?.investigationFields).to.eql({
field_names: ['host.name'],
});
});
it('should edit rules with legacy investigation fields', async () => {
const { body } = await postBulkAction().send({
query: '',
action: BulkActionType.edit,
[BulkActionType.edit]: [
{
type: BulkActionEditType.set_tags,
value: ['reset-tag'],
},
],
});
expect(body.attributes.summary).to.eql({
failed: 0,
skipped: 0,
succeeded: 3,
total: 3,
});
// Check that the updated rule is returned with the response
// and field transformed on response
const ruleWithLegacyField = body.attributes.results.updated.find(
(returnedRule: RuleResponse) =>
returnedRule.rule_id === ruleWithLegacyInvestigationField.params.ruleId
);
expect(ruleWithLegacyField.investigation_fields).to.eql({
field_names: ['client.address', 'agent.name'],
});
expect(ruleWithLegacyField.tags).to.eql(['reset-tag']);
const ruleWithEmptyArray = body.attributes.results.updated.find(
(returnedRule: RuleResponse) =>
returnedRule.rule_id === ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId
);
expect(ruleWithEmptyArray.investigation_fields).to.eql(undefined);
expect(ruleWithEmptyArray.tags).to.eql(['reset-tag']);
const ruleWithIntendedType = body.attributes.results.updated.find(
(returnedRule: RuleResponse) => returnedRule.rule_id === 'rule-with-investigation-field'
);
expect(ruleWithIntendedType.investigation_fields).to.eql({ field_names: ['host.name'] });
expect(ruleWithIntendedType.tags).to.eql(['reset-tag']);
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should not include a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, ruleWithLegacyInvestigationField.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql(['client.address', 'agent.name']);
const {
hits: {
hits: [{ _source: ruleSO2 }],
},
} = await getRuleSOById(es, ruleWithLegacyInvestigationFieldEmptyArray.id);
expect(ruleSO2?.alert?.params?.investigationFields).to.eql([]);
const {
hits: {
hits: [{ _source: ruleSO3 }],
},
} = await getRuleSOById(es, ruleWithIntendedType.id);
expect(ruleSO3?.alert?.params?.investigationFields).to.eql({ field_names: ['host.name'] });
});
});
});
};

View file

@ -6,7 +6,8 @@
*/
import expect from '@kbn/expect';
import { Rule } from '@kbn/alerting-plugin/common';
import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
import {
DETECTION_ENGINE_RULES_URL,
UPDATE_OR_CREATE_LEGACY_ACTIONS,
@ -24,6 +25,10 @@ import {
getWebHookAction,
removeServerGeneratedProperties,
removeServerGeneratedPropertiesIncludingRuleId,
getRuleSOById,
getRuleSavedObjectWithLegacyInvestigationFields,
createRuleThroughAlertingEndpoint,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
} from '../../utils';
// eslint-disable-next-line import/no-default-export
@ -267,5 +272,111 @@ export default ({ getService }: FtrProviderContext) => {
});
});
});
describe('investigation_fields', () => {
let ruleWithLegacyInvestigationField: Rule<BaseRuleParams>;
let ruleWithLegacyInvestigationFieldEmptyArray: Rule<BaseRuleParams>;
beforeEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
await createSignalsIndex(supertest, log);
ruleWithLegacyInvestigationField = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFields()
);
ruleWithLegacyInvestigationFieldEmptyArray = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray()
);
await createRule(supertest, log, {
...getSimpleRule('rule-with-investigation-field'),
name: 'Test investigation fields object',
investigation_fields: { field_names: ['host.name'] },
});
});
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('should be able to read a rule with a legacy investigation field', async () => {
const { body } = await supertest
.get(
`${DETECTION_ENGINE_RULES_URL}?rule_id=${ruleWithLegacyInvestigationField.params.ruleId}`
)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send()
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare.investigation_fields).to.eql({
field_names: ['client.address', 'agent.name'],
});
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should
* just be a transform on read, not a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, body.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql(['client.address', 'agent.name']);
});
it('should be able to read a rule with a legacy investigation field - empty array', async () => {
const { body } = await supertest
.get(
`${DETECTION_ENGINE_RULES_URL}?rule_id=${ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId}`
)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send()
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare.investigation_fields).to.eql(undefined);
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should
* just be a transform on read, not a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, body.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql([]);
});
it('does not migrate investigation fields when intended object type', async () => {
const { body } = await supertest
.get(`${DETECTION_ENGINE_RULES_URL}?rule_id=rule-with-investigation-field`)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send()
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare.investigation_fields).to.eql({ field_names: ['host.name'] });
/**
* Confirm type on SO so that it's clear in the tests whether it's expected that
* the SO itself is migrated to the inteded object type, or if the transformation is
* happening just on the response. In this case, change should
* just be a transform on read, not a migration on SO.
*/
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, body.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql({ field_names: ['host.name'] });
});
});
});
};

View file

@ -6,7 +6,8 @@
*/
import expect from '@kbn/expect';
import { Rule } from '@kbn/alerting-plugin/common';
import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
import {
DETECTION_ENGINE_RULES_URL,
NOTIFICATION_DEFAULT_FREQUENCY,
@ -35,6 +36,10 @@ import {
getThresholdRuleForSignalTesting,
getLegacyActionSO,
getSimpleRuleWithoutRuleId,
getRuleSOById,
createRuleThroughAlertingEndpoint,
getRuleSavedObjectWithLegacyInvestigationFields,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
} from '../../utils';
import {
getActionsWithFrequencies,
@ -909,5 +914,102 @@ export default ({ getService }: FtrProviderContext) => {
});
});
});
describe('legacy investigation fields', () => {
let ruleWithLegacyInvestigationField: Rule<BaseRuleParams>;
let ruleWithLegacyInvestigationFieldEmptyArray: Rule<BaseRuleParams>;
beforeEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
await createSignalsIndex(supertest, log);
ruleWithLegacyInvestigationField = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFields()
);
ruleWithLegacyInvestigationFieldEmptyArray = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray()
);
await createRule(supertest, log, {
...getSimpleRule('rule-with-investigation-field'),
name: 'Test investigation fields object',
investigation_fields: { field_names: ['host.name'] },
});
});
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('errors if sending legacy investigation fields type', async () => {
const updatedRule = {
...getSimpleRuleUpdate(ruleWithLegacyInvestigationField.params.ruleId),
investigation_fields: ['foo'],
};
const { body } = await supertest
.put(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send(updatedRule)
.expect(400);
expect(body.message).to.eql(
'[request body]: Invalid value "["foo"]" supplied to "investigation_fields"'
);
});
it('unsets legacy investigation fields when field not specified for update', async () => {
// rule_id of a rule with legacy investigation fields set
const updatedRule = getSimpleRuleUpdate(ruleWithLegacyInvestigationField.params.ruleId);
const { body } = await supertest
.put(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send(updatedRule)
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare.investigation_fields).to.eql(undefined);
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, body.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql(undefined);
});
it('updates a rule with legacy investigation fields when field specified for update in intended format', async () => {
// rule_id of a rule with legacy investigation fields set
const updatedRule = {
...getSimpleRuleUpdate(ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId),
investigation_fields: {
field_names: ['foo'],
},
};
const { body } = await supertest
.put(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send(updatedRule)
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body);
expect(bodyToCompare.investigation_fields).to.eql({
field_names: ['foo'],
});
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, body.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql({
field_names: ['foo'],
});
});
});
});
};

View file

@ -7,7 +7,8 @@
import expect from '@kbn/expect';
import { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { Rule } from '@kbn/alerting-plugin/common';
import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
import {
DETECTION_ENGINE_RULES_URL,
DETECTION_ENGINE_RULES_BULK_UPDATE,
@ -32,6 +33,10 @@ import {
removeServerGeneratedPropertiesIncludingRuleId,
getSimpleRuleWithoutRuleId,
getSimpleRuleOutputWithoutRuleId,
getRuleSavedObjectWithLegacyInvestigationFields,
createRuleThroughAlertingEndpoint,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
getRuleSOById,
} from '../../utils';
import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions';
import {
@ -803,5 +808,124 @@ export default ({ getService }: FtrProviderContext) => {
});
});
});
describe('legacy investigation fields', () => {
let ruleWithLegacyInvestigationField: Rule<BaseRuleParams>;
let ruleWithLegacyInvestigationFieldEmptyArray: Rule<BaseRuleParams>;
let ruleWithInvestigationFields: RuleResponse;
beforeEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
await createSignalsIndex(supertest, log);
ruleWithLegacyInvestigationField = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFields()
);
ruleWithLegacyInvestigationFieldEmptyArray = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray()
);
ruleWithInvestigationFields = await createRule(supertest, log, {
...getSimpleRule('rule-with-investigation-field'),
name: 'Test investigation fields object',
investigation_fields: { field_names: ['host.name'] },
});
});
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('errors if trying to update investigation fields using legacy format', async () => {
// update rule
const { body } = await supertest
.put(DETECTION_ENGINE_RULES_BULK_UPDATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([
{
...getSimpleRule(),
name: 'New name',
rule_id: ruleWithLegacyInvestigationField.params.ruleId,
investigation_fields: ['client.foo'],
},
])
.expect(400);
expect(body.message).to.eql(
'[request body]: Invalid value "["client.foo"]" supplied to "investigation_fields"'
);
});
it('updates a rule with legacy investigation fields and transforms field in response', async () => {
// update rule
const { body } = await supertest
.put(DETECTION_ENGINE_RULES_BULK_UPDATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([
{
...getSimpleRule(),
name: 'New name - used to have legacy investigation fields',
rule_id: ruleWithLegacyInvestigationField.params.ruleId,
},
{
...getSimpleRule(),
name: 'New name - used to have legacy investigation fields, empty array',
rule_id: ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId,
investigation_fields: {
field_names: ['foo'],
},
},
{
...getSimpleRule(),
name: 'New name - never had legacy investigation fields',
rule_id: 'rule-with-investigation-field',
investigation_fields: {
field_names: ['bar'],
},
},
])
.expect(200);
expect(body[0].investigation_fields).to.eql(undefined);
expect(body[0].name).to.eql('New name - used to have legacy investigation fields');
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, ruleWithLegacyInvestigationField.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql(undefined);
expect(body[1].investigation_fields).to.eql({
field_names: ['foo'],
});
expect(body[1].name).to.eql(
'New name - used to have legacy investigation fields, empty array'
);
const {
hits: {
hits: [{ _source: ruleSO2 }],
},
} = await getRuleSOById(es, ruleWithLegacyInvestigationFieldEmptyArray.id);
expect(ruleSO2?.alert?.params?.investigationFields).to.eql({
field_names: ['foo'],
});
expect(body[2].investigation_fields).to.eql({
field_names: ['bar'],
});
expect(body[2].name).to.eql('New name - never had legacy investigation fields');
const {
hits: {
hits: [{ _source: ruleSO3 }],
},
} = await getRuleSOById(es, ruleWithInvestigationFields.id);
expect(ruleSO3?.alert?.params?.investigationFields).to.eql({
field_names: ['bar'],
});
});
});
});
};

View file

@ -6,6 +6,7 @@
*/
import expect from '@kbn/expect';
import type {
ThreatMatchRuleCreateProps,
ThresholdRuleCreateProps,
@ -35,6 +36,9 @@ import {
waitForSignalsToBePresent,
updateRule,
deleteAllEventLogExecutionEvents,
getRuleSavedObjectWithLegacyInvestigationFields,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
createRuleThroughAlertingEndpoint,
} from '../../../../utils';
// eslint-disable-next-line import/no-default-export
@ -241,16 +245,25 @@ export default ({ getService }: FtrProviderContext) => {
});
describe('legacy investigation fields', () => {
before(async () => {
await esArchiver.load(
'x-pack/test/functional/es_archives/security_solution/legacy_investigation_fields'
beforeEach(async () => {
await deleteAllRules(supertest, log);
await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFields()
);
await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray()
);
await createRule(supertest, log, {
...getSimpleRule('rule-with-investigation-field'),
name: 'Test investigation fields object',
investigation_fields: { field_names: ['host.name'] },
});
});
after(async () => {
await esArchiver.unload(
'x-pack/test/functional/es_archives/security_solution/legacy_investigation_fields'
);
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('should show "legacy_investigation_fields" to be greater than 0 when a rule has "investigation_fields" set to array or empty array', async () => {

View file

@ -20,6 +20,8 @@ import {
ALERT_LAST_DETECTED,
} from '@kbn/rule-data-utils';
import { flattenWithPrefix } from '@kbn/securitysolution-rules';
import { Rule } from '@kbn/alerting-plugin/common';
import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
import { orderBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
@ -27,6 +29,7 @@ import { v4 as uuidv4 } from 'uuid';
import {
QueryRuleCreateProps,
AlertSuppressionMissingFieldsStrategy,
BulkActionType,
} from '@kbn/security-solution-plugin/common/api/detection_engine';
import { RuleExecutionStatusEnum } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
import { Ancestor } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/types';
@ -36,7 +39,11 @@ import {
ALERT_ORIGINAL_TIME,
ALERT_ORIGINAL_EVENT,
} from '@kbn/security-solution-plugin/common/field_maps/field_names';
import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '@kbn/security-solution-plugin/common/constants';
import {
DETECTION_ENGINE_RULES_BULK_ACTION,
DETECTION_ENGINE_RULES_URL,
DETECTION_ENGINE_SIGNALS_STATUS_URL,
} from '@kbn/security-solution-plugin/common/constants';
import { getMaxSignalsWarning } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/utils';
import { deleteAllExceptions } from '../../../lists_api_integration/utils';
import {
@ -51,6 +58,9 @@ import {
getSimpleRule,
previewRule,
setSignalStatus,
getRuleSOById,
createRuleThroughAlertingEndpoint,
getRuleSavedObjectWithLegacyInvestigationFields,
} from '../../utils';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { dataGeneratorFactory } from '../../utils/data_generator';
@ -2265,5 +2275,51 @@ export default ({ getService }: FtrProviderContext) => {
expect(previewAlerts[1]._source?.agent).to.have.property('name', 'test-3');
});
});
describe('legacy investigation_fields', () => {
let ruleWithLegacyInvestigationField: Rule<BaseRuleParams>;
beforeEach(async () => {
ruleWithLegacyInvestigationField = await createRuleThroughAlertingEndpoint(
supertest,
getRuleSavedObjectWithLegacyInvestigationFields()
);
});
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('should generate alerts when rule includes legacy investigation_fields', async () => {
// enable rule
await supertest
.post(DETECTION_ENGINE_RULES_BULK_ACTION)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send({ query: '', action: BulkActionType.enable })
.expect(200);
// Confirming that enabling did not migrate rule, so rule
// run/alerts generated here were from rule with legacy investigation field
const {
hits: {
hits: [{ _source: ruleSO }],
},
} = await getRuleSOById(es, ruleWithLegacyInvestigationField.id);
expect(ruleSO?.alert?.params?.investigationFields).to.eql(['client.address', 'agent.name']);
// fetch rule for format needed to pass into
const { body: ruleBody } = await supertest
.get(
`${DETECTION_ENGINE_RULES_URL}?rule_id=${ruleWithLegacyInvestigationField.params.ruleId}`
)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.expect(200);
const alertsAfterEnable = await getOpenSignals(supertest, log, es, ruleBody, 'succeeded');
expect(alertsAfterEnable.hits.hits.length > 0).eql(true);
});
});
});
};

View file

@ -0,0 +1,35 @@
/*
* 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.
*/
import type SuperTest from 'supertest';
import { Rule } from '@kbn/alerting-plugin/common';
import {
BaseRuleParams,
InternalRuleCreate,
} from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
/**
* Creates a rule using the alerting APIs directly.
* This allows us to test some legacy types that are not exposed
* on our APIs
*
* @param supertest
*/
export const createRuleThroughAlertingEndpoint = async (
supertest: SuperTest.SuperTest<SuperTest.Test>,
rule: InternalRuleCreate
): Promise<Rule<BaseRuleParams>> => {
const { body } = await supertest
.post('/api/alerting/rule')
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send(rule)
.expect(200);
return body;
};

View file

@ -10,9 +10,10 @@ import { SavedObjectReference } from '@kbn/core/server';
import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server';
import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { Rule } from '@kbn/alerting-plugin/common';
import { BaseRuleParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
interface RuleSO {
alert: Rule;
alert: Rule<BaseRuleParams>;
references: SavedObjectReference[];
}

View file

@ -0,0 +1,124 @@
/*
* 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.
*/
import { InternalRuleCreate } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_schema';
export const getRuleSavedObjectWithLegacyInvestigationFields = (): InternalRuleCreate =>
({
name: 'Test investigation fields',
tags: ['migration'],
rule_type_id: 'siem.queryRule',
consumer: 'siem',
params: {
author: [],
buildingBlockType: undefined,
falsePositives: [],
description: 'a',
ruleId: '2297be91-894c-4831-830f-b424a0ec84f0',
from: '1900-01-01T00:00:00.000Z',
immutable: false,
license: '',
outputIndex: '',
investigationFields: ['client.address', 'agent.name'],
maxSignals: 100,
meta: undefined,
riskScore: 21,
riskScoreMapping: [],
severity: 'low',
severityMapping: [],
threat: [],
to: 'now',
references: [],
timelineId: undefined,
timelineTitle: undefined,
ruleNameOverride: undefined,
timestampOverride: undefined,
timestampOverrideFallbackDisabled: undefined,
namespace: undefined,
note: undefined,
requiredFields: undefined,
version: 1,
exceptionsList: [],
relatedIntegrations: [],
setup: '',
type: 'query',
language: 'kuery',
index: ['auditbeat-*'],
query: '_id:BhbXBmkBR346wHgn4PeZ or _id:GBbXBmkBR346wHgn5_eR or _id:x10zJ2oE9v5HJNSHhyxi',
filters: [],
savedId: undefined,
responseActions: undefined,
alertSuppression: undefined,
dataViewId: undefined,
},
schedule: {
interval: '5m',
},
enabled: false,
actions: [],
throttle: null,
// cast is due to alerting API expecting rule_type_id
// and our internal schema expecting alertTypeId
} as unknown as InternalRuleCreate);
export const getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray = (): InternalRuleCreate =>
({
name: 'Test investigation fields empty array',
tags: ['migration'],
rule_type_id: 'siem.queryRule',
consumer: 'siem',
params: {
author: [],
description: 'a',
ruleId: '2297be91-894c-4831-830f-b424a0ec5678',
falsePositives: [],
from: '1900-01-01T00:00:00.000Z',
immutable: false,
license: '',
outputIndex: '',
investigationFields: [],
maxSignals: 100,
riskScore: 21,
riskScoreMapping: [],
severity: 'low',
severityMapping: [],
threat: [],
to: 'now',
references: [],
version: 1,
exceptionsList: [],
type: 'query',
language: 'kuery',
index: ['auditbeat-*'],
query: '_id:BhbXBmkBR346wHgn4PeZ or _id:GBbXBmkBR346wHgn5_eR or _id:x10zJ2oE9v5HJNSHhyxi',
filters: [],
relatedIntegrations: [],
setup: '',
buildingBlockType: undefined,
meta: undefined,
timelineId: undefined,
timelineTitle: undefined,
ruleNameOverride: undefined,
timestampOverride: undefined,
timestampOverrideFallbackDisabled: undefined,
namespace: undefined,
note: undefined,
requiredFields: undefined,
savedId: undefined,
responseActions: undefined,
alertSuppression: undefined,
dataViewId: undefined,
},
schedule: {
interval: '5m',
},
enabled: false,
actions: [],
throttle: null,
// cast is due to alerting API expecting rule_type_id
// and our internal schema expecting alertTypeId
} as unknown as InternalRuleCreate);

View file

@ -17,6 +17,7 @@ export * from './create_new_action';
export * from './create_rule';
export * from './create_rule_with_auth';
export * from './create_rule_with_exception_entries';
export * from './create_rule_saved_object';
export * from './create_signals_index';
export * from './delete_all_rules';
export * from './delete_all_event_log_execution_events';
@ -51,6 +52,7 @@ export * from './get_rule_for_signal_testing';
export * from './get_rule_so_by_id';
export * from './get_rule_for_signal_testing_with_timestamp_override';
export * from './get_rule_with_web_hook_action';
export * from './get_rule_with_legacy_investigation_fields';
export * from './get_saved_query_rule_for_signal_testing';
export * from './get_security_telemetry_stats';
export * from './get_signal_status';

View file

@ -1,271 +0,0 @@
{
"type": "doc",
"value": {
"index": ".kibana_alerting_cases",
"id": "alert:9095ee90-b075-11ec-bb3f-1f063f8e1234",
"source": {
"alert": {
"name":"Test investigation fields",
"tags":["migration"],
"alertTypeId": "siem.queryRule",
"consumer": "siem",
"revision": 0,
"params": {
"author": [],
"description": "a",
"ruleId": "2297be91-894c-4831-830f-b424a0ec84f0",
"falsePositives": [],
"from": "now-360s",
"immutable": false,
"license": "",
"outputIndex": "",
"investigationFields":["client.address","agent.name"],
"meta": {
"from": "1m",
"kibana_siem_app_url": "https://actions.kb.us-central1.gcp.cloud.es.io:9243/app/security"
},
"maxSignals": 100,
"riskScore": 21,
"riskScoreMapping": [],
"severity": "low",
"severityMapping": [],
"threat": [],
"to": "now",
"references": [],
"version": 1,
"exceptionsList": [],
"type": "query",
"language": "kuery",
"index": [
"apm-*-transaction*",
"traces-apm*",
"auditbeat-*",
"endgame-*",
"filebeat-*",
"logs-*",
"packetbeat-*",
"winlogbeat-*"
],
"query": "*:*",
"filters": []
},
"schedule": {
"interval": "5m"
},
"enabled": false,
"actions": [],
"throttle": null,
"notifyWhen": "onActiveAlert",
"apiKeyOwner": null,
"apiKey": null,
"createdBy": "1527796724",
"updatedBy": "1527796724",
"createdAt": "2022-03-30T22:05:53.511Z",
"updatedAt": "2022-03-30T22:05:53.511Z",
"muteAll": false,
"mutedInstanceIds": [],
"executionStatus": {
"status": "ok",
"lastExecutionDate": "2022-03-31T19:53:37.507Z",
"error": null,
"lastDuration": 2377
},
"meta": {
"versionApiKeyLastmodified": "8.10.0"
},
"scheduledTaskId": null,
"legacyId": "9095ee90-b075-11ec-bb3f-1f063f8e0abc"
},
"type": "alert",
"references": [],
"namespaces": [
"default"
],
"typeMigrationVersion": "8.10.0",
"coreMigrationVersion":"8.10.0",
"updated_at": "2022-03-31T19:53:39.885Z"
}
}
}
{
"type": "doc",
"value": {
"index": ".kibana_alerting_cases",
"id": "alert:9095ee90-b075-11ec-bb3f-1f063f8e5678",
"source": {
"alert": {
"name":"Test investigation fields empty array",
"tags":["migration"],
"alertTypeId": "siem.queryRule",
"consumer": "siem",
"revision": 0,
"params": {
"author": [],
"description": "a",
"ruleId": "2297be91-894c-4831-830f-b424a0ec5678",
"falsePositives": [],
"from": "now-360s",
"immutable": false,
"license": "",
"outputIndex": "",
"investigationFields":[],
"meta": {
"from": "1m",
"kibana_siem_app_url": "https://actions.kb.us-central1.gcp.cloud.es.io:9243/app/security"
},
"maxSignals": 100,
"riskScore": 21,
"riskScoreMapping": [],
"severity": "low",
"severityMapping": [],
"threat": [],
"to": "now",
"references": [],
"version": 1,
"exceptionsList": [],
"type": "query",
"language": "kuery",
"index": [
"apm-*-transaction*",
"traces-apm*",
"auditbeat-*",
"endgame-*",
"filebeat-*",
"logs-*",
"packetbeat-*",
"winlogbeat-*"
],
"query": "*:*",
"filters": []
},
"schedule": {
"interval": "5m"
},
"enabled": false,
"actions": [],
"throttle": null,
"notifyWhen": "onActiveAlert",
"apiKeyOwner": null,
"apiKey": null,
"createdBy": "1527796724",
"updatedBy": "1527796724",
"createdAt": "2022-03-30T22:05:53.511Z",
"updatedAt": "2022-03-30T22:05:53.511Z",
"muteAll": false,
"mutedInstanceIds": [],
"executionStatus": {
"status": "ok",
"lastExecutionDate": "2022-03-31T19:53:37.507Z",
"error": null,
"lastDuration": 2377
},
"meta": {
"versionApiKeyLastmodified": "8.10.0"
},
"scheduledTaskId": null,
"legacyId": "9095ee90-b075-11ec-bb3f-1f063f8e0def"
},
"type": "alert",
"references": [],
"namespaces": [
"default"
],
"typeMigrationVersion": "8.10.0",
"coreMigrationVersion":"8.10.0",
"updated_at": "2022-03-31T19:53:39.885Z"
}
}
}
{
"type": "doc",
"value": {
"index": ".kibana_alerting_cases",
"id": "alert:9095ee90-b075-11ec-bb3f-1f063f8e9102",
"source": {
"alert": {
"name":"Test investigation fields object",
"tags":["migration"],
"alertTypeId": "siem.queryRule",
"consumer": "siem",
"revision": 0,
"params": {
"author": [],
"description": "a",
"ruleId": "2297be91-894c-4831-830f-b424a0ec9102",
"falsePositives": [],
"from": "now-360s",
"immutable": false,
"license": "",
"outputIndex": "",
"investigationFields": {
"field_names": ["host.name"]
},
"meta": {
"from": "1m",
"kibana_siem_app_url": "https://actions.kb.us-central1.gcp.cloud.es.io:9243/app/security"
},
"maxSignals": 100,
"riskScore": 21,
"riskScoreMapping": [],
"severity": "low",
"severityMapping": [],
"threat": [],
"to": "now",
"references": [],
"version": 1,
"exceptionsList": [],
"type": "query",
"language": "kuery",
"index": [
"apm-*-transaction*",
"traces-apm*",
"auditbeat-*",
"endgame-*",
"filebeat-*",
"logs-*",
"packetbeat-*",
"winlogbeat-*"
],
"query": "*:*",
"filters": []
},
"schedule": {
"interval": "5m"
},
"enabled": false,
"actions": [],
"throttle": null,
"notifyWhen": "onActiveAlert",
"apiKeyOwner": null,
"apiKey": null,
"createdBy": "1527796724",
"updatedBy": "1527796724",
"createdAt": "2022-03-30T22:05:53.511Z",
"updatedAt": "2022-03-30T22:05:53.511Z",
"muteAll": false,
"mutedInstanceIds": [],
"executionStatus": {
"status": "ok",
"lastExecutionDate": "2022-03-31T19:53:37.507Z",
"error": null,
"lastDuration": 2377
},
"meta": {
"versionApiKeyLastmodified": "8.11.0"
},
"scheduledTaskId": null,
"legacyId": "9095ee90-b075-11ec-bb3f-1f063f8e0ghi"
},
"type": "alert",
"references": [],
"namespaces": [
"default"
],
"typeMigrationVersion": "8.11.0",
"coreMigrationVersion":"8.11.0",
"updated_at": "2022-03-31T19:53:39.885Z"
}
}
}