[9.0] [Security Solution] Remove bulk crud endpoints schemas (#213244) (#214608)

# Backport

This will backport the following commits from `main` to `9.0`:
- [[Security Solution] Remove bulk crud endpoints schemas
(#213244)](https://github.com/elastic/kibana/pull/213244)

<!--- Backport version: 9.6.6 -->

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

<!--BACKPORT [{"author":{"name":"Jacek
Kolezynski","email":"jacek.kolezynski@elastic.co"},"sourceCommit":{"committedDate":"2025-03-14T16:15:38Z","message":"[Security
Solution] Remove bulk crud endpoints schemas (#213244)\n\n**Partially
addresses:**
#211808,\nhttps://github.com/elastic/security-docs/issues/5981
(internal)\n**Resolves: #208329**\n\n## Summary\n\nThis is the second
part of the migration effort, containing changes for:\n- BULK CRUD
(removing, for v.9.0)\n\nThe PR also contains changes for ticket #208329
- as changes for\nremoving of dead code for handling Bulk CRUD endpoints
had to be\ncombined together with removing the schema files for Bulk
CRUD\nendpoints.\n\nThis PR will be backported only to versions for
Kibana v9\n\n# Testing\n1. cd
x-pack/solutions/security/plugins/security_solution\n2. yarn
openapi:bundle:detections \n3. Take the bundled
file\n(docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml)\nand
load it into bump.sh console to see the changes.\n4. Compare the changes
with the
[Legacy\ndocumentation](https://www.elastic.co/guide/en/security/current/rule-api-overview.html)\n\nYou
can also use this
[link](https://bump.sh/jkelas2/doc/kibana_wip2/)\nwhere I deployed the
generated bundled doc.\n\n---------\n\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"d6f71349aa7abd9b5ea413fa6460f14af28b45a6","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Detections
and Resp","Team: SecuritySolution","APIDocs","Team:Detection Rule
Management","backport:version","8.18
candidate","v9.1.0"],"title":"[Security Solution] Remove bulk crud
endpoints
schemas","number":213244,"url":"https://github.com/elastic/kibana/pull/213244","mergeCommit":{"message":"[Security
Solution] Remove bulk crud endpoints schemas (#213244)\n\n**Partially
addresses:**
#211808,\nhttps://github.com/elastic/security-docs/issues/5981
(internal)\n**Resolves: #208329**\n\n## Summary\n\nThis is the second
part of the migration effort, containing changes for:\n- BULK CRUD
(removing, for v.9.0)\n\nThe PR also contains changes for ticket #208329
- as changes for\nremoving of dead code for handling Bulk CRUD endpoints
had to be\ncombined together with removing the schema files for Bulk
CRUD\nendpoints.\n\nThis PR will be backported only to versions for
Kibana v9\n\n# Testing\n1. cd
x-pack/solutions/security/plugins/security_solution\n2. yarn
openapi:bundle:detections \n3. Take the bundled
file\n(docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml)\nand
load it into bump.sh console to see the changes.\n4. Compare the changes
with the
[Legacy\ndocumentation](https://www.elastic.co/guide/en/security/current/rule-api-overview.html)\n\nYou
can also use this
[link](https://bump.sh/jkelas2/doc/kibana_wip2/)\nwhere I deployed the
generated bundled doc.\n\n---------\n\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"d6f71349aa7abd9b5ea413fa6460f14af28b45a6"}},"sourceBranch":"main","suggestedTargetBranches":["9.0"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/213244","number":213244,"mergeCommit":{"message":"[Security
Solution] Remove bulk crud endpoints schemas (#213244)\n\n**Partially
addresses:**
#211808,\nhttps://github.com/elastic/security-docs/issues/5981
(internal)\n**Resolves: #208329**\n\n## Summary\n\nThis is the second
part of the migration effort, containing changes for:\n- BULK CRUD
(removing, for v.9.0)\n\nThe PR also contains changes for ticket #208329
- as changes for\nremoving of dead code for handling Bulk CRUD endpoints
had to be\ncombined together with removing the schema files for Bulk
CRUD\nendpoints.\n\nThis PR will be backported only to versions for
Kibana v9\n\n# Testing\n1. cd
x-pack/solutions/security/plugins/security_solution\n2. yarn
openapi:bundle:detections \n3. Take the bundled
file\n(docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml)\nand
load it into bump.sh console to see the changes.\n4. Compare the changes
with the
[Legacy\ndocumentation](https://www.elastic.co/guide/en/security/current/rule-api-overview.html)\n\nYou
can also use this
[link](https://bump.sh/jkelas2/doc/kibana_wip2/)\nwhere I deployed the
generated bundled doc.\n\n---------\n\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"d6f71349aa7abd9b5ea413fa6460f14af28b45a6"}}]}]
BACKPORT-->

Co-authored-by: Jacek Kolezynski <jacek.kolezynski@elastic.co>
This commit is contained in:
Kibana Machine 2025-03-15 05:20:51 +11:00 committed by GitHub
parent 9ebac082ac
commit 0c817d1b7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 149 additions and 6720 deletions

View file

@ -9851,177 +9851,6 @@ paths:
summary: Apply a bulk action to detection rules
tags:
- Security Detections API
/api/detection_engine/rules/_bulk_create:
post:
deprecated: true
description: Create new detection rules in bulk.
operationId: BulkCreateRules
requestBody:
content:
application/json:
schema:
items:
$ref: '#/components/schemas/Security_Detections_API_RuleCreateProps'
type: array
description: A JSON array of rules, where each rule contains the required fields.
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Security_Detections_API_BulkCrudRulesResponse'
description: Indicates a successful call.
summary: Create multiple detection rules
tags:
- Security Detections API
/api/detection_engine/rules/_bulk_delete:
delete:
deprecated: true
description: Delete detection rules in bulk.
operationId: BulkDeleteRules
requestBody:
content:
application/json:
schema:
items:
type: object
properties:
id:
$ref: '#/components/schemas/Security_Detections_API_RuleObjectId'
rule_id:
$ref: '#/components/schemas/Security_Detections_API_RuleSignatureId'
type: array
description: A JSON array of `id` or `rule_id` fields of the rules you want to delete.
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Security_Detections_API_BulkCrudRulesResponse'
description: Indicates a successful call.
'400':
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/Security_Detections_API_PlatformErrorResponse'
- $ref: '#/components/schemas/Security_Detections_API_SiemErrorResponse'
description: Invalid input data response
'401':
content:
application/json:
schema:
$ref: '#/components/schemas/Security_Detections_API_PlatformErrorResponse'
description: Unsuccessful authentication response
'500':
content:
application/json:
schema:
$ref: '#/components/schemas/Security_Detections_API_SiemErrorResponse'
description: Internal server error response
summary: Delete multiple detection rules
tags:
- Security Detections API
post:
deprecated: true
description: Deletes multiple rules.
operationId: BulkDeleteRulesPost
requestBody:
content:
application/json:
schema:
items:
type: object
properties:
id:
$ref: '#/components/schemas/Security_Detections_API_RuleObjectId'
rule_id:
$ref: '#/components/schemas/Security_Detections_API_RuleSignatureId'
type: array
description: A JSON array of `id` or `rule_id` fields of the rules you want to delete.
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Security_Detections_API_BulkCrudRulesResponse'
description: Indicates a successful call.
'400':
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/Security_Detections_API_PlatformErrorResponse'
- $ref: '#/components/schemas/Security_Detections_API_SiemErrorResponse'
description: Invalid input data response
'401':
content:
application/json:
schema:
$ref: '#/components/schemas/Security_Detections_API_PlatformErrorResponse'
description: Unsuccessful authentication response
'500':
content:
application/json:
schema:
$ref: '#/components/schemas/Security_Detections_API_SiemErrorResponse'
description: Internal server error response
summary: Delete multiple detection rules
tags:
- Security Detections API
/api/detection_engine/rules/_bulk_update:
patch:
deprecated: true
description: Update specific fields of existing detection rules using the `rule_id` or `id` field.
operationId: BulkPatchRules
requestBody:
content:
application/json:
schema:
items:
$ref: '#/components/schemas/Security_Detections_API_RulePatchProps'
type: array
description: A JSON array of rules, where each rule contains the required fields.
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Security_Detections_API_BulkCrudRulesResponse'
description: Indicates a successful call.
summary: Patch multiple detection rules
tags:
- Security Detections API
put:
deprecated: true
description: |
Update multiple detection rules using the `rule_id` or `id` field. The original rules are replaced, and all unspecified fields are deleted.
> info
> You cannot modify the `id` or `rule_id` values.
operationId: BulkUpdateRules
requestBody:
content:
application/json:
schema:
items:
$ref: '#/components/schemas/Security_Detections_API_RuleUpdateProps'
type: array
description: A JSON array where each element includes the `id` or `rule_id` field of the rule you want to update and the fields you want to modify.
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Security_Detections_API_BulkCrudRulesResponse'
description: Indicates a successful call.
summary: Update multiple detection rules
tags:
- Security Detections API
/api/detection_engine/rules/_export:
post:
description: |
@ -53037,12 +52866,6 @@ components:
required:
- id
- skip_reason
Security_Detections_API_BulkCrudRulesResponse:
items:
oneOf:
- $ref: '#/components/schemas/Security_Detections_API_RuleResponse'
- $ref: '#/components/schemas/Security_Detections_API_ErrorSchema'
type: array
Security_Detections_API_BulkDeleteRules:
type: object
properties:

View file

@ -1,27 +0,0 @@
/*
* 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.
*/
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*
* info:
* title: Bulk Create API endpoint
* version: 2023-10-31
*/
import { z } from '@kbn/zod';
import { RuleCreateProps } from '../../../model/rule_schema/rule_schemas.gen';
import { BulkCrudRulesResponse } from '../response_schema.gen';
export type BulkCreateRulesRequestBody = z.infer<typeof BulkCreateRulesRequestBody>;
export const BulkCreateRulesRequestBody = z.array(RuleCreateProps);
export type BulkCreateRulesRequestBodyInput = z.input<typeof BulkCreateRulesRequestBody>;
export type BulkCreateRulesResponse = z.infer<typeof BulkCreateRulesResponse>;
export const BulkCreateRulesResponse = BulkCrudRulesResponse;

View file

@ -1,31 +0,0 @@
openapi: 3.0.0
info:
title: Bulk Create API endpoint
version: '2023-10-31'
paths:
/api/detection_engine/rules/_bulk_create:
post:
x-labels: [ess]
x-codegen-enabled: true
operationId: BulkCreateRules
deprecated: true
summary: Create multiple detection rules
description: Create new detection rules in bulk.
tags:
- Bulk API
requestBody:
description: A JSON array of rules, where each rule contains the required fields.
required: true
content:
application/json:
schema:
type: array
items:
$ref: '../../../model/rule_schema/rule_schemas.schema.yaml#/components/schemas/RuleCreateProps'
responses:
200:
description: Indicates a successful call.
content:
application/json:
schema:
$ref: '../response_schema.schema.yaml#/components/schemas/BulkCrudRulesResponse'

View file

@ -1,165 +0,0 @@
/*
* 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 { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import { getCreateRulesSchemaMock } from '../../../model/rule_schema/mocks';
import { BulkCreateRulesRequestBody } from './bulk_create_rules_route.gen';
// only the basics of testing are here.
// see: rule_schemas.test.ts for the bulk of the validation tests
// this just wraps createRulesSchema in an array
describe('Bulk create rules request schema', () => {
test('can take an empty array and validate it', () => {
const payload: BulkCreateRulesRequestBody = [];
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('made up values do not validate for a single element', () => {
const payload: Array<{ madeUp: string }> = [{ madeUp: 'hi' }];
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.type: Invalid discriminator value. Expected 'eql' | 'query' | 'saved_query' | 'threshold' | 'threat_match' | 'machine_learning' | 'new_terms' | 'esql'"`
);
});
test('single array element does validate', () => {
const payload: BulkCreateRulesRequestBody = [getCreateRulesSchemaMock()];
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('two array elements do validate', () => {
const payload: BulkCreateRulesRequestBody = [
getCreateRulesSchemaMock(),
getCreateRulesSchemaMock(),
];
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('single array element with a missing value (risk_score) will not validate', () => {
const singleItem = getCreateRulesSchemaMock();
// @ts-expect-error
delete singleItem.risk_score;
const payload: BulkCreateRulesRequestBody = [singleItem];
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0.risk_score: Required"`);
});
test('two array elements where the first is valid but the second is invalid (risk_score) will not validate', () => {
const singleItem = getCreateRulesSchemaMock();
const secondItem = getCreateRulesSchemaMock();
// @ts-expect-error
delete secondItem.risk_score;
const payload: BulkCreateRulesRequestBody = [singleItem, secondItem];
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"1.risk_score: Required"`);
});
test('two array elements where the first is invalid (risk_score) but the second is valid will not validate', () => {
const singleItem = getCreateRulesSchemaMock();
const secondItem = getCreateRulesSchemaMock();
// @ts-expect-error
delete singleItem.risk_score;
const payload: BulkCreateRulesRequestBody = [singleItem, secondItem];
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0.risk_score: Required"`);
});
test('two array elements where both are invalid (risk_score) will not validate', () => {
const singleItem = getCreateRulesSchemaMock();
const secondItem = getCreateRulesSchemaMock();
// @ts-expect-error
delete singleItem.risk_score;
// @ts-expect-error
delete secondItem.risk_score;
const payload: BulkCreateRulesRequestBody = [singleItem, secondItem];
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.risk_score: Required, 1.risk_score: Required"`
);
});
test('extra keys are omitted from the payload', () => {
const singleItem = {
...getCreateRulesSchemaMock(),
madeUpValue: 'something',
};
const secondItem = {
...getCreateRulesSchemaMock(),
madeUpValue: 'something',
};
const payload: BulkCreateRulesRequestBody = [singleItem, secondItem];
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual([getCreateRulesSchemaMock(), getCreateRulesSchemaMock()]);
});
test('You cannot set the severity to a value other than low, medium, high, or critical', () => {
const badSeverity = { ...getCreateRulesSchemaMock(), severity: 'madeup' };
const payload = [badSeverity];
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'madeup'"`
);
});
test('You can set "note" to a string', () => {
const payload: BulkCreateRulesRequestBody = [
{ ...getCreateRulesSchemaMock(), note: '# test markdown' },
];
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('You can set "note" to an empty string', () => {
const payload: BulkCreateRulesRequestBody = [{ ...getCreateRulesSchemaMock(), note: '' }];
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('You cant set "note" to anything other than string', () => {
const payload = [
{
...getCreateRulesSchemaMock(),
note: {
something: 'some object',
},
},
];
const result = BulkCreateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.note: Expected string, received object"`
);
});
});

View file

@ -1,44 +0,0 @@
/*
* 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.
*/
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*
* info:
* title: Bulk Delete API endpoint
* version: 2023-10-31
*/
import { z } from '@kbn/zod';
import { RuleObjectId, RuleSignatureId } from '../../../model/rule_schema/common_attributes.gen';
import { BulkCrudRulesResponse } from '../response_schema.gen';
export type BulkDeleteRulesRequestBody = z.infer<typeof BulkDeleteRulesRequestBody>;
export const BulkDeleteRulesRequestBody = z.array(
z.object({
id: RuleObjectId.optional(),
rule_id: RuleSignatureId.optional(),
})
);
export type BulkDeleteRulesRequestBodyInput = z.input<typeof BulkDeleteRulesRequestBody>;
export type BulkDeleteRulesResponse = z.infer<typeof BulkDeleteRulesResponse>;
export const BulkDeleteRulesResponse = BulkCrudRulesResponse;
export type BulkDeleteRulesPostRequestBody = z.infer<typeof BulkDeleteRulesPostRequestBody>;
export const BulkDeleteRulesPostRequestBody = z.array(
z.object({
id: RuleObjectId.optional(),
rule_id: RuleSignatureId.optional(),
})
);
export type BulkDeleteRulesPostRequestBodyInput = z.input<typeof BulkDeleteRulesPostRequestBody>;
export type BulkDeleteRulesPostResponse = z.infer<typeof BulkDeleteRulesPostResponse>;
export const BulkDeleteRulesPostResponse = BulkCrudRulesResponse;

View file

@ -1,107 +0,0 @@
openapi: 3.0.0
info:
title: Bulk Delete API endpoint
version: '2023-10-31'
paths:
/api/detection_engine/rules/_bulk_delete:
delete:
x-labels: [ess]
x-codegen-enabled: true
operationId: BulkDeleteRules
deprecated: true
summary: Delete multiple detection rules
description: Delete detection rules in bulk.
tags:
- Bulk API
requestBody:
description: A JSON array of `id` or `rule_id` fields of the rules you want to delete.
required: true
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
$ref: '../../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/RuleObjectId'
rule_id:
$ref: '../../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/RuleSignatureId'
responses:
200:
description: Indicates a successful call.
content:
application/json:
schema:
$ref: '../response_schema.schema.yaml#/components/schemas/BulkCrudRulesResponse'
400:
description: Invalid input data response
content:
application/json:
schema:
oneOf:
- $ref: '../../../../model/error_responses.schema.yaml#/components/schemas/PlatformErrorResponse'
- $ref: '../../../../model/error_responses.schema.yaml#/components/schemas/SiemErrorResponse'
401:
description: Unsuccessful authentication response
content:
application/json:
schema:
$ref: '../../../../model/error_responses.schema.yaml#/components/schemas/PlatformErrorResponse'
500:
description: Internal server error response
content:
application/json:
schema:
$ref: '../../../../model/error_responses.schema.yaml#/components/schemas/SiemErrorResponse'
post:
x-labels: [ess]
x-codegen-enabled: true
operationId: BulkDeleteRulesPost
deprecated: true
summary: Delete multiple detection rules
description: Deletes multiple rules.
tags:
- Bulk API
requestBody:
description: A JSON array of `id` or `rule_id` fields of the rules you want to delete.
required: true
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
$ref: '../../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/RuleObjectId'
rule_id:
$ref: '../../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/RuleSignatureId'
responses:
200:
description: Indicates a successful call.
content:
application/json:
schema:
$ref: '../response_schema.schema.yaml#/components/schemas/BulkCrudRulesResponse'
400:
description: Invalid input data response
content:
application/json:
schema:
oneOf:
- $ref: '../../../../model/error_responses.schema.yaml#/components/schemas/PlatformErrorResponse'
- $ref: '../../../../model/error_responses.schema.yaml#/components/schemas/SiemErrorResponse'
401:
description: Unsuccessful authentication response
content:
application/json:
schema:
$ref: '../../../../model/error_responses.schema.yaml#/components/schemas/PlatformErrorResponse'
500:
description: Internal server error response
content:
application/json:
schema:
$ref: '../../../../model/error_responses.schema.yaml#/components/schemas/SiemErrorResponse'

View file

@ -1,90 +0,0 @@
/*
* 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 { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import { BulkDeleteRulesRequestBody } from './bulk_delete_rules_route.gen';
// only the basics of testing are here.
// see: query_rules_schema.test.ts for the bulk of the validation tests
// this just wraps queryRulesSchema in an array
describe('Bulk delete rules request schema', () => {
test('can take an empty array and validate it', () => {
const payload: BulkDeleteRulesRequestBody = [];
const result = BulkDeleteRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('non uuid being supplied to id does not validate', () => {
const payload: BulkDeleteRulesRequestBody = [
{
id: '1',
},
];
const result = BulkDeleteRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0.id: Invalid uuid"`);
});
test('both rule_id and id being supplied do validate', () => {
const payload: BulkDeleteRulesRequestBody = [
{
rule_id: '1',
id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f',
},
];
const result = BulkDeleteRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('only id validates with two elements', () => {
const payload: BulkDeleteRulesRequestBody = [
{ id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' },
{ id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' },
];
const result = BulkDeleteRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('only rule_id validates', () => {
const payload: BulkDeleteRulesRequestBody = [
{ rule_id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' },
];
const result = BulkDeleteRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('only rule_id validates with two elements', () => {
const payload: BulkDeleteRulesRequestBody = [
{ rule_id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' },
{ rule_id: '2' },
];
const result = BulkDeleteRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('both id and rule_id validates with two separate elements', () => {
const payload: BulkDeleteRulesRequestBody = [
{ id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f' },
{ rule_id: '2' },
];
const result = BulkDeleteRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
});

View file

@ -1,27 +0,0 @@
/*
* 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.
*/
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*
* info:
* title: Bulk Patch API endpoint
* version: 2023-10-31
*/
import { z } from '@kbn/zod';
import { RulePatchProps } from '../../../model/rule_schema/rule_schemas.gen';
import { BulkCrudRulesResponse } from '../response_schema.gen';
export type BulkPatchRulesRequestBody = z.infer<typeof BulkPatchRulesRequestBody>;
export const BulkPatchRulesRequestBody = z.array(RulePatchProps);
export type BulkPatchRulesRequestBodyInput = z.input<typeof BulkPatchRulesRequestBody>;
export type BulkPatchRulesResponse = z.infer<typeof BulkPatchRulesResponse>;
export const BulkPatchRulesResponse = BulkCrudRulesResponse;

View file

@ -1,31 +0,0 @@
openapi: 3.0.0
info:
title: Bulk Patch API endpoint
version: '2023-10-31'
paths:
/api/detection_engine/rules/_bulk_update:
patch:
x-labels: [ess]
x-codegen-enabled: true
summary: Patch multiple detection rules
operationId: BulkPatchRules
deprecated: true
description: Update specific fields of existing detection rules using the `rule_id` or `id` field.
tags:
- Bulk API
requestBody:
description: A JSON array of rules, where each rule contains the required fields.
required: true
content:
application/json:
schema:
type: array
items:
$ref: '../../../model/rule_schema/rule_schemas.schema.yaml#/components/schemas/RulePatchProps'
responses:
200:
description: Indicates a successful call.
content:
application/json:
schema:
$ref: '../response_schema.schema.yaml#/components/schemas/BulkCrudRulesResponse'

View file

@ -1,77 +0,0 @@
/*
* 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 { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import type { PatchRuleRequestBody } from '../../crud/patch_rule/patch_rule_route.gen';
import { BulkPatchRulesRequestBody } from './bulk_patch_rules_route.gen';
// only the basics of testing are here.
// see: patch_rules_schema.test.ts for the bulk of the validation tests
// this just wraps patchRulesSchema in an array
describe('Bulk patch rules request schema', () => {
test('can take an empty array and validate it', () => {
const payload: BulkPatchRulesRequestBody = [];
const result = BulkPatchRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('single array of [id] does validate', () => {
const payload: BulkPatchRulesRequestBody = [{ id: '4125761e-51da-4de9-a0c8-42824f532ddb' }];
const result = BulkPatchRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('two arrays of [id] validate', () => {
const payload: BulkPatchRulesRequestBody = [
{ id: '4125761e-51da-4de9-a0c8-42824f532ddb' },
{ id: '192f403d-b285-4251-9e8b-785fcfcf22e8' },
];
const result = BulkPatchRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('can set "note" to be a string', () => {
const payload: BulkPatchRulesRequestBody = [
{ id: '4125761e-51da-4de9-a0c8-42824f532ddb' },
{ note: 'hi' },
];
const result = BulkPatchRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('can set "note" to be an empty string', () => {
const payload: BulkPatchRulesRequestBody = [
{ id: '4125761e-51da-4de9-a0c8-42824f532ddb' },
{ note: '' },
];
const result = BulkPatchRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('cannot set "note" to be anything other than a string', () => {
const payload: Array<Omit<PatchRuleRequestBody, 'note'> & { note?: object }> = [
{ id: '4125761e-51da-4de9-a0c8-42824f532ddb' },
{ note: { someprop: 'some value here' } },
];
const result = BulkPatchRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"1.note: Expected string, received object, 1.note: Expected string, received object, 1.note: Expected string, received object, 1.note: Expected string, received object, 1.note: Expected string, received object, and 3 more"`
);
});
});

View file

@ -1,27 +0,0 @@
/*
* 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.
*/
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*
* info:
* title: Bulk Update API endpoint
* version: 2023-10-31
*/
import { z } from '@kbn/zod';
import { RuleUpdateProps } from '../../../model/rule_schema/rule_schemas.gen';
import { BulkCrudRulesResponse } from '../response_schema.gen';
export type BulkUpdateRulesRequestBody = z.infer<typeof BulkUpdateRulesRequestBody>;
export const BulkUpdateRulesRequestBody = z.array(RuleUpdateProps);
export type BulkUpdateRulesRequestBodyInput = z.input<typeof BulkUpdateRulesRequestBody>;
export type BulkUpdateRulesResponse = z.infer<typeof BulkUpdateRulesResponse>;
export const BulkUpdateRulesResponse = BulkCrudRulesResponse;

View file

@ -1,34 +0,0 @@
openapi: 3.0.0
info:
title: Bulk Update API endpoint
version: '2023-10-31'
paths:
/api/detection_engine/rules/_bulk_update:
put:
x-labels: [ess]
x-codegen-enabled: true
operationId: BulkUpdateRules
deprecated: true
summary: Update multiple detection rules
description: |
Update multiple detection rules using the `rule_id` or `id` field. The original rules are replaced, and all unspecified fields are deleted.
> info
> You cannot modify the `id` or `rule_id` values.
tags:
- Bulk API
requestBody:
description: A JSON array where each element includes the `id` or `rule_id` field of the rule you want to update and the fields you want to modify.
required: true
content:
application/json:
schema:
type: array
items:
$ref: '../../../model/rule_schema/rule_schemas.schema.yaml#/components/schemas/RuleUpdateProps'
responses:
200:
description: Indicates a successful call.
content:
application/json:
schema:
$ref: '../response_schema.schema.yaml#/components/schemas/BulkCrudRulesResponse'

View file

@ -1,176 +0,0 @@
/*
* 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 { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import type { RuleUpdateProps } from '../../../model';
import { getUpdateRulesSchemaMock } from '../../../model/rule_schema/mocks';
import { BulkUpdateRulesRequestBody } from './bulk_update_rules_route.gen';
// only the basics of testing are here.
// see: update_rules_schema.test.ts for the bulk of the validation tests
// this just wraps updateRulesSchema in an array
describe('Bulk update rules request schema', () => {
test('can take an empty array and validate it', () => {
const payload: BulkUpdateRulesRequestBody = [];
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('made up values do not validate for a single element', () => {
const payload: Array<{ madeUp: string }> = [{ madeUp: 'hi' }];
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.type: Invalid discriminator value. Expected 'eql' | 'query' | 'saved_query' | 'threshold' | 'threat_match' | 'machine_learning' | 'new_terms' | 'esql'"`
);
});
test('single array element does validate', () => {
const payload: BulkUpdateRulesRequestBody = [getUpdateRulesSchemaMock()];
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('two array elements do validate', () => {
const payload: BulkUpdateRulesRequestBody = [
getUpdateRulesSchemaMock(),
getUpdateRulesSchemaMock(),
];
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('single array element with a missing value (risk_score) will not validate', () => {
const singleItem = getUpdateRulesSchemaMock();
// @ts-expect-error
delete singleItem.risk_score;
const payload: BulkUpdateRulesRequestBody = [singleItem];
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0.risk_score: Required"`);
});
test('two array elements where the first is valid but the second is invalid (risk_score) will not validate', () => {
const singleItem = getUpdateRulesSchemaMock();
const secondItem = getUpdateRulesSchemaMock();
// @ts-expect-error
delete secondItem.risk_score;
const payload: BulkUpdateRulesRequestBody = [singleItem, secondItem];
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"1.risk_score: Required"`);
});
test('two array elements where the first is invalid (risk_score) but the second is valid will not validate', () => {
const singleItem = getUpdateRulesSchemaMock();
const secondItem = getUpdateRulesSchemaMock();
// @ts-expect-error
delete singleItem.risk_score;
const payload: BulkUpdateRulesRequestBody = [singleItem, secondItem];
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(`"0.risk_score: Required"`);
});
test('two array elements where both are invalid (risk_score) will not validate', () => {
const singleItem = getUpdateRulesSchemaMock();
const secondItem = getUpdateRulesSchemaMock();
// @ts-expect-error
delete singleItem.risk_score;
// @ts-expect-error
delete secondItem.risk_score;
const payload: BulkUpdateRulesRequestBody = [singleItem, secondItem];
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.risk_score: Required, 1.risk_score: Required"`
);
});
test('extra props will be omitted from the payload after validation', () => {
const singleItem: RuleUpdateProps & { madeUpValue: string } = {
...getUpdateRulesSchemaMock(),
madeUpValue: 'something',
};
const secondItem: RuleUpdateProps & { madeUpValue: string } = {
...getUpdateRulesSchemaMock(),
madeUpValue: 'something',
};
const payload: BulkUpdateRulesRequestBody = [singleItem, secondItem];
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual([getUpdateRulesSchemaMock(), getUpdateRulesSchemaMock()]);
});
test('You cannot set the severity to a value other than low, medium, high, or critical', () => {
const badSeverity = { ...getUpdateRulesSchemaMock(), severity: 'madeup' };
const payload = [badSeverity];
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.severity: Invalid enum value. Expected 'low' | 'medium' | 'high' | 'critical', received 'madeup'"`
);
});
test('You can set "namespace" to a string', () => {
const payload: BulkUpdateRulesRequestBody = [
{ ...getUpdateRulesSchemaMock(), namespace: 'a namespace' },
];
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('You can set "note" to a string', () => {
const payload: BulkUpdateRulesRequestBody = [
{ ...getUpdateRulesSchemaMock(), note: '# test markdown' },
];
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('You can set "note" to an empty string', () => {
const payload: BulkUpdateRulesRequestBody = [{ ...getUpdateRulesSchemaMock(), note: '' }];
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('You cant set "note" to anything other than string', () => {
const payload = [
{
...getUpdateRulesSchemaMock(),
note: {
something: 'some object',
},
},
];
const result = BulkUpdateRulesRequestBody.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.note: Expected string, received object"`
);
});
});

View file

@ -1,23 +0,0 @@
/*
* 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.
*/
/*
* NOTICE: Do not edit this file manually.
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
*
* info:
* title: Bulk Response Schema
* version: 8.9.0
*/
import { z } from '@kbn/zod';
import { RuleResponse } from '../../model/rule_schema/rule_schemas.gen';
import { ErrorSchema } from '../../model/error_schema.gen';
export type BulkCrudRulesResponse = z.infer<typeof BulkCrudRulesResponse>;
export const BulkCrudRulesResponse = z.array(z.union([RuleResponse, ErrorSchema]));

View file

@ -1,14 +0,0 @@
openapi: 3.0.0
info:
title: Bulk Response Schema
version: 8.9.0
paths: {}
components:
x-codegen-enabled: true
schemas:
BulkCrudRulesResponse:
type: array
items:
oneOf:
- $ref: '../../model/rule_schema/rule_schemas.schema.yaml#/components/schemas/RuleResponse'
- $ref: '../../model/error_schema.schema.yaml#/components/schemas/ErrorSchema'

View file

@ -1,111 +0,0 @@
/*
* 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 { ErrorSchema, RuleResponse } from '../../model';
import { getErrorSchemaMock } from '../../model/error_schema.mock';
import { getRulesSchemaMock } from '../../model/rule_schema/mocks';
import { expectParseError, expectParseSuccess, stringifyZodError } from '@kbn/zod-helpers';
import { BulkCrudRulesResponse } from './response_schema.gen';
describe('Bulk CRUD rules response schema', () => {
test('it should validate a regular message and and error together with a uuid', () => {
const payload: BulkCrudRulesResponse = [getRulesSchemaMock(), getErrorSchemaMock()];
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should validate a regular message and error together when the error has a non UUID', () => {
const payload: BulkCrudRulesResponse = [getRulesSchemaMock(), getErrorSchemaMock('fake id')];
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should validate an error', () => {
const payload: BulkCrudRulesResponse = [getErrorSchemaMock('fake id')];
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual(payload);
});
test('it should NOT validate a rule with a deleted value', () => {
const rule = getRulesSchemaMock();
// @ts-expect-error
delete rule.name;
const payload: BulkCrudRulesResponse = [rule];
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.name: Required, 0.error: Required, 0: Unrecognized key(s) in object: 'author', 'created_at', 'updated_at', 'created_by', 'description', 'enabled', 'false_positives', 'from', 'immutable', 'references', 'revision', 'severity', 'severity_mapping', 'updated_by', 'tags', 'to', 'threat', 'version', 'output_index', 'max_signals', 'risk_score', 'risk_score_mapping', 'rule_source', 'interval', 'exceptions_list', 'related_integrations', 'required_fields', 'setup', 'throttle', 'actions', 'building_block_type', 'note', 'license', 'outcome', 'alias_target_id', 'alias_purpose', 'timeline_id', 'timeline_title', 'meta', 'rule_name_override', 'timestamp_override', 'timestamp_override_fallback_disabled', 'namespace', 'investigation_fields', 'query', 'type', 'language', 'index', 'data_view_id', 'filters', 'saved_id', 'response_actions', 'alert_suppression'"`
);
});
test('it should NOT validate an invalid error message with a deleted value', () => {
const error = getErrorSchemaMock('fake id');
// @ts-expect-error
delete error.error;
const payload: BulkCrudRulesResponse = [error];
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0.type: Invalid discriminator value. Expected 'eql' | 'query' | 'saved_query' | 'threshold' | 'threat_match' | 'machine_learning' | 'new_terms' | 'esql', 0.error: Required"`
);
});
test('it should omit any extra rule props', () => {
const rule: RuleResponse & { invalid_extra_data?: string } = getRulesSchemaMock();
rule.invalid_extra_data = 'invalid_extra_data';
const payload: BulkCrudRulesResponse = [rule];
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual([getRulesSchemaMock()]);
});
test('it should NOT validate a type of "query" when it has extra data next to a valid error', () => {
const rule: RuleResponse & { invalid_extra_data?: string } = getRulesSchemaMock();
rule.invalid_extra_data = 'invalid_extra_data';
const payload: BulkCrudRulesResponse = [getErrorSchemaMock(), rule];
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseSuccess(result);
expect(result.data).toEqual([getErrorSchemaMock(), getRulesSchemaMock()]);
});
test('it should NOT validate an error when it has extra data', () => {
type InvalidError = ErrorSchema & { invalid_extra_data?: string };
const error: InvalidError = getErrorSchemaMock();
error.invalid_extra_data = 'invalid';
const payload: BulkCrudRulesResponse = [error];
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"0: Unrecognized key(s) in object: 'invalid_extra_data'"`
);
});
test('it should NOT validate an error when it has extra data next to a valid payload element', () => {
type InvalidError = ErrorSchema & { invalid_extra_data?: string };
const error: InvalidError = getErrorSchemaMock();
error.invalid_extra_data = 'invalid';
const payload: BulkCrudRulesResponse = [getRulesSchemaMock(), error];
const result = BulkCrudRulesResponse.safeParse(payload);
expectParseError(result);
expect(stringifyZodError(result.error)).toMatchInlineSnapshot(
`"1: Unrecognized key(s) in object: 'invalid_extra_data'"`
);
});
});

View file

@ -7,11 +7,6 @@
export * from './bulk_actions/bulk_actions_types';
export * from './bulk_actions/bulk_actions_route.gen';
export * from './bulk_crud/bulk_create_rules/bulk_create_rules_route.gen';
export * from './bulk_crud/bulk_delete_rules/bulk_delete_rules_route.gen';
export * from './bulk_crud/bulk_patch_rules/bulk_patch_rules_route.gen';
export * from './bulk_crud/bulk_update_rules/bulk_update_rules_route.gen';
export * from './bulk_crud/response_schema.gen';
export * from './coverage_overview/coverage_overview_route';
export * from './crud/create_rule/create_rule_route.gen';
export * from './crud/create_rule/request_schema_validation';

View file

@ -37,24 +37,6 @@ import type {
PerformRulesBulkActionRequestBodyInput,
PerformRulesBulkActionResponse,
} from './detection_engine/rule_management/bulk_actions/bulk_actions_route.gen';
import type {
BulkCreateRulesRequestBodyInput,
BulkCreateRulesResponse,
} from './detection_engine/rule_management/bulk_crud/bulk_create_rules/bulk_create_rules_route.gen';
import type {
BulkDeleteRulesRequestBodyInput,
BulkDeleteRulesResponse,
BulkDeleteRulesPostRequestBodyInput,
BulkDeleteRulesPostResponse,
} from './detection_engine/rule_management/bulk_crud/bulk_delete_rules/bulk_delete_rules_route.gen';
import type {
BulkPatchRulesRequestBodyInput,
BulkPatchRulesResponse,
} from './detection_engine/rule_management/bulk_crud/bulk_patch_rules/bulk_patch_rules_route.gen';
import type {
BulkUpdateRulesRequestBodyInput,
BulkUpdateRulesResponse,
} from './detection_engine/rule_management/bulk_crud/bulk_update_rules/bulk_update_rules_route.gen';
import type {
CreateRuleRequestBodyInput,
CreateRuleResponse,
@ -472,89 +454,6 @@ after 30 days. It also deletes other artifacts specific to the migration impleme
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Create new detection rules in bulk.
*/
async bulkCreateRules(props: BulkCreateRulesProps) {
this.log.info(`${new Date().toISOString()} Calling API BulkCreateRules`);
return this.kbnClient
.request<BulkCreateRulesResponse>({
path: '/api/detection_engine/rules/_bulk_create',
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
},
method: 'POST',
body: props.body,
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Delete detection rules in bulk.
*/
async bulkDeleteRules(props: BulkDeleteRulesProps) {
this.log.info(`${new Date().toISOString()} Calling API BulkDeleteRules`);
return this.kbnClient
.request<BulkDeleteRulesResponse>({
path: '/api/detection_engine/rules/_bulk_delete',
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
},
method: 'DELETE',
body: props.body,
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Deletes multiple rules.
*/
async bulkDeleteRulesPost(props: BulkDeleteRulesPostProps) {
this.log.info(`${new Date().toISOString()} Calling API BulkDeleteRulesPost`);
return this.kbnClient
.request<BulkDeleteRulesPostResponse>({
path: '/api/detection_engine/rules/_bulk_delete',
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
},
method: 'POST',
body: props.body,
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Update specific fields of existing detection rules using the `rule_id` or `id` field.
*/
async bulkPatchRules(props: BulkPatchRulesProps) {
this.log.info(`${new Date().toISOString()} Calling API BulkPatchRules`);
return this.kbnClient
.request<BulkPatchRulesResponse>({
path: '/api/detection_engine/rules/_bulk_update',
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
},
method: 'PATCH',
body: props.body,
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Update multiple detection rules using the `rule_id` or `id` field. The original rules are replaced, and all unspecified fields are deleted.
> info
> You cannot modify the `id` or `rule_id` values.
*/
async bulkUpdateRules(props: BulkUpdateRulesProps) {
this.log.info(`${new Date().toISOString()} Calling API BulkUpdateRules`);
return this.kbnClient
.request<BulkUpdateRulesResponse>({
path: '/api/detection_engine/rules/_bulk_update',
headers: {
[ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31',
},
method: 'PUT',
body: props.body,
})
.catch(catchAxiosErrorFormatAndThrow);
}
/**
* Bulk upsert up to 1000 asset criticality records.
@ -2324,21 +2223,6 @@ detection engine rules.
export interface AlertsMigrationCleanupProps {
body: AlertsMigrationCleanupRequestBodyInput;
}
export interface BulkCreateRulesProps {
body: BulkCreateRulesRequestBodyInput;
}
export interface BulkDeleteRulesProps {
body: BulkDeleteRulesRequestBodyInput;
}
export interface BulkDeleteRulesPostProps {
body: BulkDeleteRulesPostRequestBodyInput;
}
export interface BulkPatchRulesProps {
body: BulkPatchRulesRequestBodyInput;
}
export interface BulkUpdateRulesProps {
body: BulkUpdateRulesRequestBodyInput;
}
export interface BulkUpsertAssetCriticalityRecordsProps {
body: BulkUpsertAssetCriticalityRecordsRequestBodyInput;
}

View file

@ -245,12 +245,6 @@ export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags` as const
export const DETECTION_ENGINE_RULES_BULK_ACTION =
`${DETECTION_ENGINE_RULES_URL}/_bulk_action` as const;
export const DETECTION_ENGINE_RULES_PREVIEW = `${DETECTION_ENGINE_RULES_URL}/preview` as const;
export const DETECTION_ENGINE_RULES_BULK_DELETE =
`${DETECTION_ENGINE_RULES_URL}/_bulk_delete` as const;
export const DETECTION_ENGINE_RULES_BULK_CREATE =
`${DETECTION_ENGINE_RULES_URL}/_bulk_create` as const;
export const DETECTION_ENGINE_RULES_BULK_UPDATE =
`${DETECTION_ENGINE_RULES_URL}/_bulk_update` as const;
export const DETECTION_ENGINE_RULES_IMPORT_URL = `${DETECTION_ENGINE_RULES_URL}/_import` as const;
export * from './entity_analytics/constants';

View file

@ -393,193 +393,6 @@ paths:
tags:
- Security Detections API
- Bulk API
/api/detection_engine/rules/_bulk_create:
post:
deprecated: true
description: Create new detection rules in bulk.
operationId: BulkCreateRules
requestBody:
content:
application/json:
schema:
items:
$ref: '#/components/schemas/RuleCreateProps'
type: array
description: A JSON array of rules, where each rule contains the required fields.
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/BulkCrudRulesResponse'
description: Indicates a successful call.
summary: Create multiple detection rules
tags:
- Security Detections API
- Bulk API
/api/detection_engine/rules/_bulk_delete:
delete:
deprecated: true
description: Delete detection rules in bulk.
operationId: BulkDeleteRules
requestBody:
content:
application/json:
schema:
items:
type: object
properties:
id:
$ref: '#/components/schemas/RuleObjectId'
rule_id:
$ref: '#/components/schemas/RuleSignatureId'
type: array
description: >-
A JSON array of `id` or `rule_id` fields of the rules you want to
delete.
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/BulkCrudRulesResponse'
description: Indicates a successful call.
'400':
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/PlatformErrorResponse'
- $ref: '#/components/schemas/SiemErrorResponse'
description: Invalid input data response
'401':
content:
application/json:
schema:
$ref: '#/components/schemas/PlatformErrorResponse'
description: Unsuccessful authentication response
'500':
content:
application/json:
schema:
$ref: '#/components/schemas/SiemErrorResponse'
description: Internal server error response
summary: Delete multiple detection rules
tags:
- Security Detections API
- Bulk API
post:
deprecated: true
description: Deletes multiple rules.
operationId: BulkDeleteRulesPost
requestBody:
content:
application/json:
schema:
items:
type: object
properties:
id:
$ref: '#/components/schemas/RuleObjectId'
rule_id:
$ref: '#/components/schemas/RuleSignatureId'
type: array
description: >-
A JSON array of `id` or `rule_id` fields of the rules you want to
delete.
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/BulkCrudRulesResponse'
description: Indicates a successful call.
'400':
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/PlatformErrorResponse'
- $ref: '#/components/schemas/SiemErrorResponse'
description: Invalid input data response
'401':
content:
application/json:
schema:
$ref: '#/components/schemas/PlatformErrorResponse'
description: Unsuccessful authentication response
'500':
content:
application/json:
schema:
$ref: '#/components/schemas/SiemErrorResponse'
description: Internal server error response
summary: Delete multiple detection rules
tags:
- Security Detections API
- Bulk API
/api/detection_engine/rules/_bulk_update:
patch:
deprecated: true
description: >-
Update specific fields of existing detection rules using the `rule_id`
or `id` field.
operationId: BulkPatchRules
requestBody:
content:
application/json:
schema:
items:
$ref: '#/components/schemas/RulePatchProps'
type: array
description: A JSON array of rules, where each rule contains the required fields.
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/BulkCrudRulesResponse'
description: Indicates a successful call.
summary: Patch multiple detection rules
tags:
- Security Detections API
- Bulk API
put:
deprecated: true
description: >
Update multiple detection rules using the `rule_id` or `id` field. The
original rules are replaced, and all unspecified fields are deleted.
> info
> You cannot modify the `id` or `rule_id` values.
operationId: BulkUpdateRules
requestBody:
content:
application/json:
schema:
items:
$ref: '#/components/schemas/RuleUpdateProps'
type: array
description: >-
A JSON array where each element includes the `id` or `rule_id` field
of the rule you want to update and the fields you want to modify.
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/BulkCrudRulesResponse'
description: Indicates a successful call.
summary: Update multiple detection rules
tags:
- Security Detections API
- Bulk API
/api/detection_engine/rules/_export:
post:
description: >
@ -2118,12 +1931,6 @@ components:
required:
- id
- skip_reason
BulkCrudRulesResponse:
items:
oneOf:
- $ref: '#/components/schemas/RuleResponse'
- $ref: '#/components/schemas/ErrorSchema'
type: array
BulkDeleteRules:
type: object
properties:

View file

@ -83,11 +83,17 @@ import { buildCreateRuleExceptionListItemsProps } from './modules/exceptions';
// ... omitted client setup stuff
// Core logic
const ruleCopies = duplicateRuleParams(basicRule, 200);
const response = await detectionsClient.bulkCreateRules({ body: ruleCopies });
const createdRules: RuleResponse[] = response.data.filter(
(r) => r.id != null
) as RuleResponse[];
const bodyWithCreatedRule = await createRule(supertest, log, basicRule);
const ruleCopies = duplicateRuleParams(bodyWithCreatedRule, 200);
const { body } = await detectionsClient.
.performRulesBulkAction({
query: { dry_run: false },
body: {
ids: ruleCopies.map((rule) => rule.id),
action: 'duplicate',
},
})
const createdRules: RuleResponse[] = body.attributes.results.created;
// This map looks a bit confusing, but the concept is simple: take the rules we just created and
// create a *function* per rule to create an exception for that rule. We want a function to call later instead of just

View file

@ -25,9 +25,6 @@ import {
DETECTION_ENGINE_SIGNALS_FINALIZE_MIGRATION_URL,
DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL,
DETECTION_ENGINE_RULES_BULK_ACTION,
DETECTION_ENGINE_RULES_BULK_UPDATE,
DETECTION_ENGINE_RULES_BULK_DELETE,
DETECTION_ENGINE_RULES_BULK_CREATE,
DETECTION_ENGINE_RULES_URL_FIND,
DETECTION_ENGINE_RULES_IMPORT_URL,
} from '../../../../../common/constants';
@ -117,27 +114,6 @@ export const getFindRequest = () =>
path: DETECTION_ENGINE_RULES_URL_FIND,
});
export const getReadBulkRequest = () =>
requestMock.create({
method: 'post',
path: DETECTION_ENGINE_RULES_BULK_CREATE,
body: [getCreateRulesSchemaMock()],
});
export const getUpdateBulkRequest = () =>
requestMock.create({
method: 'put',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [getCreateRulesSchemaMock()],
});
export const getPatchBulkRequest = () =>
requestMock.create({
method: 'patch',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [getCreateRulesSchemaMock()],
});
export const getBulkDisableRuleActionRequest = () =>
requestMock.create({
method: 'patch',
@ -152,34 +128,6 @@ export const getBulkActionEditRequest = () =>
body: getPerformBulkActionEditSchemaMock(),
});
export const getDeleteBulkRequest = () =>
requestMock.create({
method: 'delete',
path: DETECTION_ENGINE_RULES_BULK_DELETE,
body: [{ rule_id: 'rule-1' }],
});
export const getDeleteBulkRequestById = () =>
requestMock.create({
method: 'delete',
path: DETECTION_ENGINE_RULES_BULK_DELETE,
body: [{ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd' }],
});
export const getDeleteAsPostBulkRequestById = () =>
requestMock.create({
method: 'post',
path: DETECTION_ENGINE_RULES_BULK_DELETE,
body: [{ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd' }],
});
export const getDeleteAsPostBulkRequest = () =>
requestMock.create({
method: 'post',
path: DETECTION_ENGINE_RULES_BULK_DELETE,
body: [{ rule_id: 'rule-1' }],
});
export const getPrivilegeRequest = (options: { auth?: { isAuthenticated: boolean } } = {}) =>
requestMock.create({
method: 'get',

View file

@ -1,43 +0,0 @@
/*
* 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 { getDocLinks } from '@kbn/doc-links';
import type { Logger } from '@kbn/core/server';
import { DETECTION_ENGINE_RULES_BULK_ACTION } from '../../../../../common/constants';
/**
* Helper method for building deprecation messages
*
* @param path Deprecated endpoint path
* @returns string
*/
export const buildDeprecatedBulkEndpointMessage = (path: string) => {
const docsLink = getDocLinks({ kibanaBranch: 'main', buildFlavor: 'traditional' }).siem
.ruleApiOverview;
return `Deprecated endpoint: ${path} API is deprecated since v8.2. Please use the ${DETECTION_ENGINE_RULES_BULK_ACTION} API instead. See ${docsLink} for more detail.`;
};
/**
* Logs usages of a deprecated bulk endpoint
*
* @param logger System logger
* @param path Deprecated endpoint path
*/
export const logDeprecatedBulkEndpoint = (logger: Logger, path: string) => {
logger.warn(buildDeprecatedBulkEndpointMessage(path), { tags: ['deprecation'] });
};
/**
* Creates a warning header with a message formatted according to RFC7234.
* We follow the same formatting as Elasticsearch
* https://github.com/elastic/elasticsearch/blob/5baabff6670a8ed49297488ca8cac8ec12a2078d/server/src/main/java/org/elasticsearch/common/logging/HeaderWarning.java#L55
*
* @param path Deprecated endpoint path
*/
export const getDeprecatedBulkEndpointHeader = (path: string) => ({
warning: `299 Kibana "${buildDeprecatedBulkEndpointMessage(path)}"`,
});

View file

@ -1,43 +0,0 @@
/*
* 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 { BulkCreateRulesRequestBody } from '../../../../../../../common/api/detection_engine/rule_management';
import { getDuplicates } from './get_duplicates';
describe('getDuplicates', () => {
test("returns array of ruleIds showing the duplicate keys of 'value2' and 'value3'", () => {
const output = getDuplicates(
[
{ rule_id: 'value1' },
{ rule_id: 'value2' },
{ rule_id: 'value2' },
{ rule_id: 'value3' },
{ rule_id: 'value3' },
{},
{},
] as BulkCreateRulesRequestBody,
'rule_id'
);
const expected = ['value2', 'value3'];
expect(output).toEqual(expected);
});
test('returns null when given a map of no duplicates', () => {
const output = getDuplicates(
[
{ rule_id: 'value1' },
{ rule_id: 'value2' },
{ rule_id: 'value3' },
{},
{},
] as BulkCreateRulesRequestBody,
'rule_id'
);
const expected: string[] = [];
expect(output).toEqual(expected);
});
});

View file

@ -1,24 +0,0 @@
/*
* 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 { countBy } from 'lodash/fp';
import type { BulkCreateRulesRequestBody } from '../../../../../../../common/api/detection_engine/rule_management';
export const getDuplicates = (
ruleDefinitions: BulkCreateRulesRequestBody,
by: 'rule_id'
): string[] => {
const mappedDuplicates = countBy(
by,
ruleDefinitions.filter((r) => r[by] != null)
);
const hasDuplicates = Object.values(mappedDuplicates).some((i) => i > 1);
if (hasDuplicates) {
return Object.keys(mappedDuplicates).filter((key) => mappedDuplicates[key] > 1);
}
return [];
};

View file

@ -1,202 +0,0 @@
/*
* 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 { DETECTION_ENGINE_RULES_BULK_CREATE } from '../../../../../../../common/constants';
import {
getReadBulkRequest,
getFindResultWithSingleHit,
getEmptyFindResult,
getRuleMock,
createBulkMlRuleRequest,
getBasicEmptySearchResponse,
getBasicNoShardsSearchResponse,
} from '../../../../routes/__mocks__/request_responses';
import { requestContextMock, serverMock, requestMock } from '../../../../routes/__mocks__';
import { bulkCreateRulesRoute } from './route';
import { getCreateRulesSchemaMock } from '../../../../../../../common/api/detection_engine/model/rule_schema/mocks';
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import { getQueryRuleParams } from '../../../../rule_schema/mocks';
import { loggingSystemMock, docLinksServiceMock } from '@kbn/core/server/mocks';
import { HttpAuthzError } from '../../../../../machine_learning/validation';
import { getRulesSchemaMock } from '../../../../../../../common/api/detection_engine/model/rule_schema/rule_response_schema.mock';
describe('Bulk create rules route', () => {
let server: ReturnType<typeof serverMock.create>;
let { clients, context } = requestContextMock.createTools();
beforeEach(() => {
server = serverMock.create();
({ clients, context } = requestContextMock.createTools());
const logger = loggingSystemMock.createLogger();
const docLinks = docLinksServiceMock.createSetupContract();
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); // no existing rules
clients.rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams())); // successful creation
clients.detectionRulesClient.createCustomRule.mockResolvedValue(getRulesSchemaMock());
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse())
);
bulkCreateRulesRoute(server.router, logger, docLinks);
});
describe('status codes', () => {
test('returns 200', async () => {
const response = await server.inject(
getReadBulkRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
});
});
describe('unhappy paths', () => {
test('returns a 403 error object if ML Authz fails', async () => {
clients.detectionRulesClient.createCustomRule.mockImplementationOnce(async () => {
throw new HttpAuthzError('mocked validation message');
});
const response = await server.inject(
createBulkMlRuleRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
expect(response.body).toEqual([
{
error: {
message: 'mocked validation message',
status_code: 403,
},
rule_id: 'rule-1',
},
]);
});
test('returns an error object if the index does not exist when rule registry not enabled', async () => {
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValueOnce(
elasticsearchClientMock.createSuccessTransportRequestPromise(
getBasicNoShardsSearchResponse()
)
);
const response = await server.inject(
getReadBulkRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
});
test('returns a duplicate error if rule_id already exists', async () => {
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
const response = await server.inject(
getReadBulkRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
expect(response.body).toEqual([
expect.objectContaining({
error: {
message: expect.stringContaining('already exists'),
status_code: 409,
},
}),
]);
});
test('catches error if creation throws', async () => {
clients.detectionRulesClient.createCustomRule.mockImplementation(async () => {
throw new Error('Test error');
});
const response = await server.inject(
getReadBulkRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
expect(response.body).toEqual([
expect.objectContaining({
error: {
message: 'Test error',
status_code: 500,
},
}),
]);
});
test('returns an error object if duplicate rule_ids found in request payload', async () => {
const request = requestMock.create({
method: 'post',
path: DETECTION_ENGINE_RULES_BULK_CREATE,
body: [getCreateRulesSchemaMock(), getCreateRulesSchemaMock()],
});
const response = await server.inject(request, requestContextMock.convertContext(context));
expect(response.status).toEqual(200);
expect(response.body).toEqual([
expect.objectContaining({
error: {
message: expect.stringContaining('already exists'),
status_code: 409,
},
}),
]);
});
});
describe('request validation', () => {
test('allows rule type of query', async () => {
const request = requestMock.create({
method: 'post',
path: DETECTION_ENGINE_RULES_BULK_CREATE,
body: [{ ...getCreateRulesSchemaMock(), type: 'query' }],
});
const result = server.validate(request);
expect(result.ok).toHaveBeenCalled();
});
test('allows rule type of query and custom from and interval', async () => {
const request = requestMock.create({
method: 'post',
path: DETECTION_ENGINE_RULES_BULK_CREATE,
body: [{ from: 'now-7m', interval: '5m', ...getCreateRulesSchemaMock() }],
});
const result = server.validate(request);
expect(result.ok).toHaveBeenCalled();
});
test('disallows unknown rule type', async () => {
const request = requestMock.create({
method: 'post',
path: DETECTION_ENGINE_RULES_BULK_CREATE,
body: [{ ...getCreateRulesSchemaMock(), type: 'unexpected_type' }],
});
const result = server.validate(request);
expect(result.badRequest).toHaveBeenCalled();
});
test('disallows invalid "from" param on rule', async () => {
const request = requestMock.create({
method: 'post',
path: DETECTION_ENGINE_RULES_BULK_CREATE,
body: [
{
from: 'now-3755555555555555.67s',
interval: '5m',
...getCreateRulesSchemaMock(),
},
],
});
const result = server.validate(request);
expect(result.badRequest).toHaveBeenCalledWith(
'0.from: Failed to parse date-math expression'
);
});
});
});

View file

@ -1,175 +0,0 @@
/*
* 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 { DocLinksServiceSetup, IKibanaResponse, Logger } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import {
DETECTION_ENGINE_RULES_BULK_CREATE,
DETECTION_ENGINE_RULES_IMPORT_URL,
} from '../../../../../../../common/constants';
import {
BulkCreateRulesRequestBody,
validateCreateRuleProps,
BulkCrudRulesResponse,
} from '../../../../../../../common/api/detection_engine/rule_management';
import type { SecuritySolutionPluginRouter } from '../../../../../../types';
import { readRules } from '../../../logic/detection_rules_client/read_rules';
import { getDuplicates } from './get_duplicates';
import { validateRuleDefaultExceptionList } from '../../../logic/exceptions/validate_rule_default_exception_list';
import { validateRulesWithDuplicatedDefaultExceptionsList } from '../../../logic/exceptions/validate_rules_with_duplicated_default_exceptions_list';
import { RULE_MANAGEMENT_BULK_ACTION_SOCKET_TIMEOUT_MS } from '../../timeouts';
import {
transformBulkError,
createBulkErrorObject,
buildSiemResponse,
} from '../../../../routes/utils';
import { getDeprecatedBulkEndpointHeader, logDeprecatedBulkEndpoint } from '../../deprecation';
/**
* @deprecated since version 8.2.0. Use the detection_engine/rules/_bulk_action API instead
*
* TODO: https://github.com/elastic/kibana/issues/193184 Delete this route and clean up the code
*/
export const bulkCreateRulesRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger,
docLinks: DocLinksServiceSetup
) => {
const securityDocLinks = docLinks.links.securitySolution;
router.versioned
.post({
access: 'public',
path: DETECTION_ENGINE_RULES_BULK_CREATE,
security: {
authz: {
requiredPrivileges: ['securitySolution'],
},
},
options: {
timeout: {
idleSocket: RULE_MANAGEMENT_BULK_ACTION_SOCKET_TIMEOUT_MS,
},
},
})
.addVersion(
{
version: '2023-10-31',
validate: {
request: {
body: buildRouteValidationWithZod(BulkCreateRulesRequestBody),
},
},
options: {
deprecated: {
documentationUrl: securityDocLinks.legacyRuleManagementBulkApiDeprecations,
severity: 'warning',
reason: {
type: 'migrate',
newApiMethod: 'POST',
newApiPath: DETECTION_ENGINE_RULES_IMPORT_URL,
},
},
},
},
async (context, request, response): Promise<IKibanaResponse<BulkCrudRulesResponse>> => {
logDeprecatedBulkEndpoint(logger, DETECTION_ENGINE_RULES_BULK_CREATE);
const siemResponse = buildSiemResponse(response);
try {
const ctx = await context.resolve(['core', 'securitySolution', 'licensing', 'alerting']);
const rulesClient = await ctx.alerting.getRulesClient();
const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient();
const ruleDefinitions = request.body;
const dupes = getDuplicates(ruleDefinitions, 'rule_id');
const rules = await Promise.all(
ruleDefinitions
.filter((rule) => rule.rule_id == null || !dupes.includes(rule.rule_id))
.map(async (payloadRule) => {
if (payloadRule.rule_id != null) {
const rule = await readRules({
id: undefined,
rulesClient,
ruleId: payloadRule.rule_id,
});
if (rule != null) {
return createBulkErrorObject({
ruleId: payloadRule.rule_id,
statusCode: 409,
message: `rule_id: "${payloadRule.rule_id}" already exists`,
});
}
}
try {
validateRulesWithDuplicatedDefaultExceptionsList({
allRules: request.body,
exceptionsList: payloadRule.exceptions_list,
ruleId: payloadRule.rule_id,
});
await validateRuleDefaultExceptionList({
exceptionsList: payloadRule.exceptions_list,
rulesClient,
ruleRuleId: payloadRule.rule_id,
ruleId: undefined,
});
const validationErrors = validateCreateRuleProps(payloadRule);
if (validationErrors.length) {
return createBulkErrorObject({
ruleId: payloadRule.rule_id,
statusCode: 400,
message: validationErrors.join(),
});
}
const createdRule = await detectionRulesClient.createCustomRule({
params: payloadRule,
});
return createdRule;
} catch (err) {
return transformBulkError(
payloadRule.rule_id,
err as Error & { statusCode?: number }
);
}
})
);
const rulesBulk = [
...rules,
...dupes.map((ruleId) =>
createBulkErrorObject({
ruleId,
statusCode: 409,
message: `rule_id: "${ruleId}" already exists`,
})
),
];
return response.ok({
body: BulkCrudRulesResponse.parse(rulesBulk),
headers: getDeprecatedBulkEndpointHeader(DETECTION_ENGINE_RULES_BULK_CREATE),
});
} catch (err) {
const error = transformError(err);
return siemResponse.error({
body: error.message,
headers: getDeprecatedBulkEndpointHeader(DETECTION_ENGINE_RULES_BULK_CREATE),
statusCode: error.statusCode,
});
}
}
);
};

View file

@ -1,144 +0,0 @@
/*
* 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 { DETECTION_ENGINE_RULES_BULK_DELETE } from '../../../../../../../common/constants';
import {
getEmptyFindResult,
getFindResultWithSingleHit,
getDeleteBulkRequest,
getDeleteBulkRequestById,
getDeleteAsPostBulkRequest,
getDeleteAsPostBulkRequestById,
getEmptySavedObjectsResponse,
} from '../../../../routes/__mocks__/request_responses';
import { requestContextMock, serverMock, requestMock } from '../../../../routes/__mocks__';
import { bulkDeleteRulesRoute } from './route';
import { loggingSystemMock, docLinksServiceMock } from '@kbn/core/server/mocks';
describe('Bulk delete rules route', () => {
let server: ReturnType<typeof serverMock.create>;
let { clients, context } = requestContextMock.createTools();
beforeEach(() => {
server = serverMock.create();
({ clients, context } = requestContextMock.createTools());
const logger = loggingSystemMock.createLogger();
const docLinks = docLinksServiceMock.createSetupContract();
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists
clients.rulesClient.delete.mockResolvedValue({}); // successful deletion
clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); // rule status request
bulkDeleteRulesRoute(server.router, logger, docLinks);
});
describe('status codes with actionClient and alertClient', () => {
test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId', async () => {
const response = await server.inject(
getDeleteBulkRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
});
test('returns 200 when deleting a single rule and related rule status', async () => {
const response = await server.inject(
getDeleteBulkRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
});
test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId using POST', async () => {
const response = await server.inject(
getDeleteAsPostBulkRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
});
test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => {
const response = await server.inject(
getDeleteBulkRequestById(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
});
test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id using POST', async () => {
const response = await server.inject(
getDeleteAsPostBulkRequestById(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
});
test('returns 200 because the error is in the payload when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => {
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult());
const response = await server.inject(
getDeleteBulkRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
});
test('returns 404 in the payload when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => {
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult());
const response = await server.inject(
getDeleteBulkRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
expect(response.body).toEqual(
expect.arrayContaining([
{
error: { message: 'rule_id: "rule-1" not found', status_code: 404 },
rule_id: 'rule-1',
},
])
);
});
});
describe('request validation', () => {
test('rejects requests without IDs', async () => {
const request = requestMock.create({
method: 'post',
path: DETECTION_ENGINE_RULES_BULK_DELETE,
body: [{}],
});
const response = await server.inject(request, requestContextMock.convertContext(context));
expect(response.status).toEqual(200);
expect(response.body).toEqual([
{
error: { message: 'either "id" or "rule_id" must be set', status_code: 400 },
rule_id: '(unknown id)',
},
]);
});
test('rejects requests with both id and rule_id', async () => {
const request = requestMock.create({
method: 'post',
path: DETECTION_ENGINE_RULES_BULK_DELETE,
body: [{ id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f', rule_id: 'rule_1' }],
});
const response = await server.inject(request, requestContextMock.convertContext(context));
expect(response.status).toEqual(200);
expect(response.body).toEqual([
{
error: {
message: 'both "id" and "rule_id" cannot exist, choose one or the other',
status_code: 400,
},
rule_id: 'c1e1b359-7ac1-4e96-bc81-c683c092436f',
},
]);
});
});
});

View file

@ -1,183 +0,0 @@
/*
* 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 { VersionedRouteConfig } from '@kbn/core-http-server';
import type {
DocLinksServiceSetup,
IKibanaResponse,
Logger,
RequestHandler,
} from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import type {
BulkDeleteRulesPostResponse,
BulkDeleteRulesResponse,
} from '../../../../../../../common/api/detection_engine/rule_management';
import {
BulkCrudRulesResponse,
BulkDeleteRulesPostRequestBody,
BulkDeleteRulesRequestBody,
validateQueryRuleByIds,
} from '../../../../../../../common/api/detection_engine/rule_management';
import {
DETECTION_ENGINE_RULES_BULK_ACTION,
DETECTION_ENGINE_RULES_BULK_DELETE,
} from '../../../../../../../common/constants';
import type {
SecuritySolutionPluginRouter,
SecuritySolutionRequestHandlerContext,
} from '../../../../../../types';
import {
buildSiemResponse,
createBulkErrorObject,
transformBulkError,
} from '../../../../routes/utils';
import { readRules } from '../../../logic/detection_rules_client/read_rules';
import { getIdBulkError } from '../../../utils/utils';
import { transformValidateBulkError } from '../../../utils/validate';
import { getDeprecatedBulkEndpointHeader, logDeprecatedBulkEndpoint } from '../../deprecation';
import { RULE_MANAGEMENT_BULK_ACTION_SOCKET_TIMEOUT_MS } from '../../timeouts';
type Handler = RequestHandler<
unknown,
unknown,
BulkDeleteRulesRequestBody | BulkDeleteRulesPostRequestBody,
SecuritySolutionRequestHandlerContext,
'delete' | 'post'
>;
/**
* @deprecated since version 8.2.0. Use the detection_engine/rules/_bulk_action API instead
*
* TODO: https://github.com/elastic/kibana/issues/193184 Delete this route and clean up the code
*/
export const bulkDeleteRulesRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger,
docLinks: DocLinksServiceSetup
) => {
const handler: Handler = async (
context,
request,
response
): Promise<IKibanaResponse<BulkDeleteRulesResponse | BulkDeleteRulesPostResponse>> => {
logDeprecatedBulkEndpoint(logger, DETECTION_ENGINE_RULES_BULK_DELETE);
const siemResponse = buildSiemResponse(response);
try {
const ctx = await context.resolve(['core', 'securitySolution', 'alerting']);
const rulesClient = await ctx.alerting.getRulesClient();
const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient();
const rules = await Promise.all(
request.body.map(async (payloadRule) => {
const { id, rule_id: ruleId } = payloadRule;
const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)';
const validationErrors = validateQueryRuleByIds(payloadRule);
if (validationErrors.length) {
return createBulkErrorObject({
ruleId: idOrRuleIdOrUnknown,
statusCode: 400,
message: validationErrors.join(),
});
}
try {
const rule = await readRules({ rulesClient, id, ruleId });
if (!rule) {
return getIdBulkError({ id, ruleId });
}
await detectionRulesClient.deleteRule({
ruleId: rule.id,
});
return transformValidateBulkError(idOrRuleIdOrUnknown, rule);
} catch (err) {
return transformBulkError(idOrRuleIdOrUnknown, err);
}
})
);
return response.ok({
body: BulkCrudRulesResponse.parse(rules),
headers: getDeprecatedBulkEndpointHeader(DETECTION_ENGINE_RULES_BULK_DELETE),
});
} catch (err) {
const error = transformError(err);
return siemResponse.error({
body: error.message,
headers: getDeprecatedBulkEndpointHeader(DETECTION_ENGINE_RULES_BULK_DELETE),
statusCode: error.statusCode,
});
}
};
const routeConfig: VersionedRouteConfig<'post' | 'delete'> = {
access: 'public',
path: DETECTION_ENGINE_RULES_BULK_DELETE,
options: {
timeout: {
idleSocket: RULE_MANAGEMENT_BULK_ACTION_SOCKET_TIMEOUT_MS,
},
},
security: {
authz: { requiredPrivileges: ['securitySolution'] },
},
};
const securityDocLinks = docLinks.links.securitySolution;
router.versioned.delete(routeConfig).addVersion(
{
version: '2023-10-31',
validate: {
request: {
body: buildRouteValidationWithZod(BulkDeleteRulesRequestBody),
},
},
options: {
deprecated: {
documentationUrl: securityDocLinks.legacyRuleManagementBulkApiDeprecations,
severity: 'warning',
reason: {
type: 'migrate',
newApiMethod: 'POST',
newApiPath: DETECTION_ENGINE_RULES_BULK_ACTION,
},
},
},
},
handler
);
router.versioned.post(routeConfig).addVersion(
{
version: '2023-10-31',
validate: {
request: {
body: buildRouteValidationWithZod(BulkDeleteRulesPostRequestBody),
},
},
options: {
deprecated: {
documentationUrl: securityDocLinks.legacyRuleManagementBulkApiDeprecations,
severity: 'warning',
reason: {
type: 'migrate',
newApiMethod: 'POST',
newApiPath: DETECTION_ENGINE_RULES_BULK_ACTION,
},
},
},
},
handler
);
};

View file

@ -1,228 +0,0 @@
/*
* 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 {
DETECTION_ENGINE_RULES_BULK_UPDATE,
DETECTION_ENGINE_RULES_URL,
} from '../../../../../../../common/constants';
import {
getEmptyFindResult,
getFindResultWithSingleHit,
getPatchBulkRequest,
getRuleMock,
typicalMlRulePayload,
} from '../../../../routes/__mocks__/request_responses';
import { serverMock, requestContextMock, requestMock } from '../../../../routes/__mocks__';
import {
getRulesSchemaMock,
getRulesMlSchemaMock,
} from '../../../../../../../common/api/detection_engine/model/rule_schema/rule_response_schema.mock';
import { bulkPatchRulesRoute } from './route';
import { getCreateRulesSchemaMock } from '../../../../../../../common/api/detection_engine/model/rule_schema/mocks';
import { getMlRuleParams, getQueryRuleParams } from '../../../../rule_schema/mocks';
import { loggingSystemMock, docLinksServiceMock } from '@kbn/core/server/mocks';
import { HttpAuthzError } from '../../../../../machine_learning/validation';
describe('Bulk patch rules route', () => {
let server: ReturnType<typeof serverMock.create>;
let { clients, context } = requestContextMock.createTools();
beforeEach(() => {
server = serverMock.create();
({ clients, context } = requestContextMock.createTools());
const logger = loggingSystemMock.createLogger();
const docLinks = docLinksServiceMock.createSetupContract();
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists
clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); // update succeeds
clients.detectionRulesClient.patchRule.mockResolvedValue(getRulesSchemaMock());
bulkPatchRulesRoute(server.router, logger, docLinks);
});
describe('status codes', () => {
test('returns 200', async () => {
const response = await server.inject(
getPatchBulkRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
});
test('returns an error in the response when updating a single rule that does not exist', async () => {
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult());
const response = await server.inject(
getPatchBulkRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
expect(response.body).toEqual([
{
error: { message: 'rule_id: "rule-1" not found', status_code: 404 },
rule_id: 'rule-1',
},
]);
});
test('allows ML Params to be patched', async () => {
const anomalyThreshold = 4;
const machineLearningJobId = 'some_job_id';
clients.rulesClient.get.mockResolvedValueOnce(getRuleMock(getMlRuleParams()));
clients.rulesClient.find.mockResolvedValueOnce({
...getFindResultWithSingleHit(),
data: [getRuleMock(getMlRuleParams())],
});
clients.detectionRulesClient.patchRule.mockResolvedValueOnce({
...getRulesMlSchemaMock(),
anomaly_threshold: anomalyThreshold,
machine_learning_job_id: [machineLearningJobId],
});
const request = requestMock.create({
method: 'patch',
path: `${DETECTION_ENGINE_RULES_URL}/bulk_update`,
body: [
{
type: 'machine_learning',
rule_id: 'my-rule-id',
anomaly_threshold: anomalyThreshold,
machine_learning_job_id: machineLearningJobId,
},
],
});
const response = await server.inject(request, requestContextMock.convertContext(context));
expect(response.status).toEqual(200);
expect(response.body[0].machine_learning_job_id).toEqual([machineLearningJobId]);
expect(response.body[0].anomaly_threshold).toEqual(anomalyThreshold);
});
it('rejects patching a rule to ML if mlAuthz fails', async () => {
clients.detectionRulesClient.patchRule.mockImplementationOnce(async () => {
throw new HttpAuthzError('mocked validation message');
});
const request = requestMock.create({
method: 'patch',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [typicalMlRulePayload()],
});
const response = await server.inject(request, requestContextMock.convertContext(context));
expect(response.status).toEqual(200);
expect(response.body).toEqual([
{
error: {
message: 'mocked validation message',
status_code: 403,
},
rule_id: 'rule-1',
},
]);
});
it('rejects patching an existing ML rule if mlAuthz fails', async () => {
clients.detectionRulesClient.patchRule.mockImplementationOnce(async () => {
throw new HttpAuthzError('mocked validation message');
});
const { type, ...payloadWithoutType } = typicalMlRulePayload();
const request = requestMock.create({
method: 'patch',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [payloadWithoutType],
});
const response = await server.inject(request, requestContextMock.convertContext(context));
expect(response.status).toEqual(200);
expect(response.body).toEqual([
{
error: {
message: 'mocked validation message',
status_code: 403,
},
rule_id: 'rule-1',
},
]);
});
});
describe('request validation', () => {
test('rejects payloads with no ID', async () => {
const request = requestMock.create({
method: 'patch',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [{ ...getCreateRulesSchemaMock(), rule_id: undefined }],
});
const response = await server.inject(request, requestContextMock.convertContext(context));
expect(response.status).toEqual(200);
expect(response.body).toEqual([
{
error: {
message: 'id or rule_id should have been defined',
status_code: 404,
},
rule_id: '(unknown id)',
},
]);
});
test('allows query rule type', async () => {
const request = requestMock.create({
method: 'patch',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [{ ...getCreateRulesSchemaMock(), type: 'query' }],
});
const result = server.validate(request);
expect(result.ok).toHaveBeenCalled();
});
test('rejects unknown rule type', async () => {
const request = requestMock.create({
method: 'patch',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [{ ...getCreateRulesSchemaMock(), type: 'unknown_type' }],
});
const result = server.validate(request);
expect(result.badRequest).toHaveBeenCalledWith(
'0.type: Invalid literal value, expected "eql", 0.language: Invalid literal value, expected "eql", 0.type: Invalid literal value, expected "query", 0.type: Invalid literal value, expected "saved_query", 0.type: Invalid literal value, expected "threshold", and 5 more'
);
});
test('allows rule type of query and custom from and interval', async () => {
const request = requestMock.create({
method: 'patch',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [{ from: 'now-7m', interval: '5m', ...getCreateRulesSchemaMock() }],
});
const result = server.validate(request);
expect(result.ok).toHaveBeenCalled();
});
test('disallows invalid "from" param on rule', async () => {
const request = requestMock.create({
method: 'patch',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [
{
from: 'now-3755555555555555.67s',
interval: '5m',
...getCreateRulesSchemaMock(),
},
],
});
const result = server.validate(request);
expect(result.badRequest).toHaveBeenCalledWith(
'0.from: Failed to parse date-math expression'
);
});
});
});

View file

@ -1,139 +0,0 @@
/*
* 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 { DocLinksServiceSetup, IKibanaResponse, Logger } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import {
DETECTION_ENGINE_RULES_BULK_ACTION,
DETECTION_ENGINE_RULES_BULK_UPDATE,
} from '../../../../../../../common/constants';
import {
BulkPatchRulesRequestBody,
BulkCrudRulesResponse,
} from '../../../../../../../common/api/detection_engine/rule_management';
import type { SecuritySolutionPluginRouter } from '../../../../../../types';
import { transformBulkError, buildSiemResponse } from '../../../../routes/utils';
import { getIdBulkError } from '../../../utils/utils';
import { readRules } from '../../../logic/detection_rules_client/read_rules';
import { getDeprecatedBulkEndpointHeader, logDeprecatedBulkEndpoint } from '../../deprecation';
import { validateRuleDefaultExceptionList } from '../../../logic/exceptions/validate_rule_default_exception_list';
import { validateRulesWithDuplicatedDefaultExceptionsList } from '../../../logic/exceptions/validate_rules_with_duplicated_default_exceptions_list';
import { RULE_MANAGEMENT_BULK_ACTION_SOCKET_TIMEOUT_MS } from '../../timeouts';
/**
* @deprecated since version 8.2.0. Use the detection_engine/rules/_bulk_action API instead
*
* TODO: https://github.com/elastic/kibana/issues/193184 Delete this route and clean up the code
*/
export const bulkPatchRulesRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger,
docLinks: DocLinksServiceSetup
) => {
const securityDocLinks = docLinks.links.securitySolution;
router.versioned
.patch({
access: 'public',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
security: {
authz: {
requiredPrivileges: ['securitySolution'],
},
},
options: {
timeout: {
idleSocket: RULE_MANAGEMENT_BULK_ACTION_SOCKET_TIMEOUT_MS,
},
},
})
.addVersion(
{
version: '2023-10-31',
validate: {
request: {
body: buildRouteValidationWithZod(BulkPatchRulesRequestBody),
},
},
options: {
deprecated: {
documentationUrl: securityDocLinks.legacyRuleManagementBulkApiDeprecations,
severity: 'warning',
reason: {
type: 'migrate',
newApiMethod: 'POST',
newApiPath: DETECTION_ENGINE_RULES_BULK_ACTION,
},
},
},
},
async (context, request, response): Promise<IKibanaResponse<BulkCrudRulesResponse>> => {
logDeprecatedBulkEndpoint(logger, DETECTION_ENGINE_RULES_BULK_UPDATE);
const siemResponse = buildSiemResponse(response);
try {
const ctx = await context.resolve(['core', 'securitySolution', 'alerting', 'licensing']);
const rulesClient = await ctx.alerting.getRulesClient();
const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient();
const rules = await Promise.all(
request.body.map(async (payloadRule) => {
const idOrRuleIdOrUnknown = payloadRule.id ?? payloadRule.rule_id ?? '(unknown id)';
try {
const existingRule = await readRules({
rulesClient,
ruleId: payloadRule.rule_id,
id: payloadRule.id,
});
if (!existingRule) {
return getIdBulkError({ id: payloadRule.id, ruleId: payloadRule.rule_id });
}
validateRulesWithDuplicatedDefaultExceptionsList({
allRules: request.body,
exceptionsList: payloadRule.exceptions_list,
ruleId: idOrRuleIdOrUnknown,
});
await validateRuleDefaultExceptionList({
exceptionsList: payloadRule.exceptions_list,
rulesClient,
ruleRuleId: payloadRule.rule_id,
ruleId: payloadRule.id,
});
const patchedRule = await detectionRulesClient.patchRule({
rulePatch: payloadRule,
});
return patchedRule;
} catch (err) {
return transformBulkError(idOrRuleIdOrUnknown, err);
}
})
);
return response.ok({
body: BulkCrudRulesResponse.parse(rules),
headers: getDeprecatedBulkEndpointHeader(DETECTION_ENGINE_RULES_BULK_UPDATE),
});
} catch (err) {
const error = transformError(err);
return siemResponse.error({
body: error.message,
headers: getDeprecatedBulkEndpointHeader(DETECTION_ENGINE_RULES_BULK_UPDATE),
statusCode: error.statusCode,
});
}
}
);
};

View file

@ -1,182 +0,0 @@
/*
* 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 { DETECTION_ENGINE_RULES_BULK_UPDATE } from '../../../../../../../common/constants';
import {
getEmptyFindResult,
getRuleMock,
getFindResultWithSingleHit,
getUpdateBulkRequest,
typicalMlRulePayload,
} from '../../../../routes/__mocks__/request_responses';
import { serverMock, requestContextMock, requestMock } from '../../../../routes/__mocks__';
import { getRulesSchemaMock } from '../../../../../../../common/api/detection_engine/model/rule_schema/rule_response_schema.mock';
import { bulkUpdateRulesRoute } from './route';
import type { BulkError } from '../../../../routes/utils';
import { getCreateRulesSchemaMock } from '../../../../../../../common/api/detection_engine/model/rule_schema/mocks';
import { getQueryRuleParams } from '../../../../rule_schema/mocks';
import { loggingSystemMock, docLinksServiceMock } from '@kbn/core/server/mocks';
import { HttpAuthzError } from '../../../../../machine_learning/validation';
describe('Bulk update rules route', () => {
let server: ReturnType<typeof serverMock.create>;
let { clients, context } = requestContextMock.createTools();
beforeEach(() => {
server = serverMock.create();
({ clients, context } = requestContextMock.createTools());
const logger = loggingSystemMock.createLogger();
const docLinks = docLinksServiceMock.createSetupContract();
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams()));
clients.detectionRulesClient.updateRule.mockResolvedValue(getRulesSchemaMock());
clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index');
bulkUpdateRulesRoute(server.router, logger, docLinks);
});
describe('status codes', () => {
test('returns 200', async () => {
const response = await server.inject(
getUpdateBulkRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
});
test('returns 200 as a response when updating a single rule that does not exist', async () => {
clients.rulesClient.find.mockResolvedValue(getEmptyFindResult());
const expected: BulkError[] = [
{
error: { message: 'rule_id: "rule-1" not found', status_code: 404 },
rule_id: 'rule-1',
},
];
const response = await server.inject(
getUpdateBulkRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
expect(response.body).toEqual(expected);
});
test('returns an error if update throws', async () => {
clients.detectionRulesClient.updateRule.mockImplementation(() => {
throw new Error('Test error');
});
const expected: BulkError[] = [
{
error: { message: 'Test error', status_code: 500 },
rule_id: 'rule-1',
},
];
const response = await server.inject(
getUpdateBulkRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
expect(response.body).toEqual(expected);
});
it('returns a 403 error object if mlAuthz fails', async () => {
clients.detectionRulesClient.updateRule.mockImplementationOnce(async () => {
throw new HttpAuthzError('mocked validation message');
});
const request = requestMock.create({
method: 'put',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [typicalMlRulePayload()],
});
const response = await server.inject(request, requestContextMock.convertContext(context));
expect(response.status).toEqual(200);
expect(response.body).toEqual([
{
error: {
message: 'mocked validation message',
status_code: 403,
},
rule_id: 'rule-1',
},
]);
});
});
describe('request validation', () => {
test('rejects payloads with no ID', async () => {
const noIdRequest = requestMock.create({
method: 'put',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [{ ...getCreateRulesSchemaMock(), rule_id: undefined }],
});
const response = await server.inject(noIdRequest, requestContextMock.convertContext(context));
expect(response.body).toEqual([
{
error: { message: 'either "id" or "rule_id" must be set', status_code: 400 },
rule_id: '(unknown id)',
},
]);
});
test('allows query rule type', async () => {
const request = requestMock.create({
method: 'put',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [{ ...getCreateRulesSchemaMock(), type: 'query' }],
});
const result = server.validate(request);
expect(result.ok).toHaveBeenCalled();
});
test('rejects unknown rule type', async () => {
const request = requestMock.create({
method: 'put',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [{ ...getCreateRulesSchemaMock(), type: 'unknown_type' }],
});
const result = server.validate(request);
expect(result.badRequest).toHaveBeenCalled();
});
test('allows rule type of query and custom from and interval', async () => {
const request = requestMock.create({
method: 'put',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [{ from: 'now-7m', interval: '5m', ...getCreateRulesSchemaMock(), type: 'query' }],
});
const result = server.validate(request);
expect(result.ok).toHaveBeenCalled();
});
test('disallows invalid "from" param on rule', async () => {
const request = requestMock.create({
method: 'put',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
body: [
{
from: 'now-3755555555555555.67s',
interval: '5m',
...getCreateRulesSchemaMock(),
type: 'query',
},
],
});
const result = server.validate(request);
expect(result.badRequest).toHaveBeenCalledWith(
'0.from: Failed to parse date-math expression'
);
});
});
});

View file

@ -1,150 +0,0 @@
/*
* 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 { DocLinksServiceSetup, IKibanaResponse, Logger } from '@kbn/core/server';
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
import { transformError } from '@kbn/securitysolution-es-utils';
import {
BulkUpdateRulesRequestBody,
validateUpdateRuleProps,
BulkCrudRulesResponse,
} from '../../../../../../../common/api/detection_engine/rule_management';
import type { SecuritySolutionPluginRouter } from '../../../../../../types';
import {
DETECTION_ENGINE_RULES_BULK_ACTION,
DETECTION_ENGINE_RULES_BULK_UPDATE,
} from '../../../../../../../common/constants';
import { getIdBulkError } from '../../../utils/utils';
import {
transformBulkError,
buildSiemResponse,
createBulkErrorObject,
} from '../../../../routes/utils';
import { readRules } from '../../../logic/detection_rules_client/read_rules';
import { getDeprecatedBulkEndpointHeader, logDeprecatedBulkEndpoint } from '../../deprecation';
import { validateRuleDefaultExceptionList } from '../../../logic/exceptions/validate_rule_default_exception_list';
import { validateRulesWithDuplicatedDefaultExceptionsList } from '../../../logic/exceptions/validate_rules_with_duplicated_default_exceptions_list';
import { RULE_MANAGEMENT_BULK_ACTION_SOCKET_TIMEOUT_MS } from '../../timeouts';
/**
* @deprecated since version 8.2.0. Use the detection_engine/rules/_bulk_action API instead
*
* TODO: https://github.com/elastic/kibana/issues/193184 Delete this route and clean up the code
*/
export const bulkUpdateRulesRoute = (
router: SecuritySolutionPluginRouter,
logger: Logger,
docLinks: DocLinksServiceSetup
) => {
const securityDocLinks = docLinks.links.securitySolution;
router.versioned
.put({
access: 'public',
path: DETECTION_ENGINE_RULES_BULK_UPDATE,
security: {
authz: {
requiredPrivileges: ['securitySolution'],
},
},
options: {
timeout: {
idleSocket: RULE_MANAGEMENT_BULK_ACTION_SOCKET_TIMEOUT_MS,
},
},
})
.addVersion(
{
version: '2023-10-31',
validate: {
request: {
body: buildRouteValidationWithZod(BulkUpdateRulesRequestBody),
},
},
options: {
deprecated: {
documentationUrl: securityDocLinks.legacyRuleManagementBulkApiDeprecations,
severity: 'warning',
reason: {
type: 'migrate',
newApiMethod: 'POST',
newApiPath: DETECTION_ENGINE_RULES_BULK_ACTION,
},
},
},
},
async (context, request, response): Promise<IKibanaResponse<BulkCrudRulesResponse>> => {
logDeprecatedBulkEndpoint(logger, DETECTION_ENGINE_RULES_BULK_UPDATE);
const siemResponse = buildSiemResponse(response);
try {
const ctx = await context.resolve(['core', 'securitySolution', 'alerting', 'licensing']);
const rulesClient = await ctx.alerting.getRulesClient();
const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient();
const rules = await Promise.all(
request.body.map(async (payloadRule) => {
const idOrRuleIdOrUnknown = payloadRule.id ?? payloadRule.rule_id ?? '(unknown id)';
try {
const validationErrors = validateUpdateRuleProps(payloadRule);
if (validationErrors.length) {
return createBulkErrorObject({
ruleId: payloadRule.rule_id,
statusCode: 400,
message: validationErrors.join(),
});
}
const existingRule = await readRules({
rulesClient,
ruleId: payloadRule.rule_id,
id: payloadRule.id,
});
if (!existingRule) {
return getIdBulkError({ id: payloadRule.id, ruleId: payloadRule.rule_id });
}
validateRulesWithDuplicatedDefaultExceptionsList({
allRules: request.body,
exceptionsList: payloadRule.exceptions_list,
ruleId: idOrRuleIdOrUnknown,
});
await validateRuleDefaultExceptionList({
exceptionsList: payloadRule.exceptions_list,
rulesClient,
ruleRuleId: payloadRule.rule_id,
ruleId: payloadRule.id,
});
const updatedRule = await detectionRulesClient.updateRule({
ruleUpdate: payloadRule,
});
return updatedRule;
} catch (err) {
return transformBulkError(idOrRuleIdOrUnknown, err);
}
})
);
return response.ok({
body: BulkCrudRulesResponse.parse(rules),
headers: getDeprecatedBulkEndpointHeader(DETECTION_ENGINE_RULES_BULK_UPDATE),
});
} catch (err) {
const error = transformError(err);
return siemResponse.error({
body: error.message,
headers: getDeprecatedBulkEndpointHeader(DETECTION_ENGINE_RULES_BULK_UPDATE),
statusCode: error.statusCode,
});
}
}
);
};

View file

@ -1,90 +0,0 @@
/*
* 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 { List } from '@kbn/securitysolution-io-ts-list-types';
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { validateRulesWithDuplicatedDefaultExceptionsList } from './validate_rules_with_duplicated_default_exceptions_list';
const notDefaultExceptionList: List = {
id: '1',
list_id: '2345',
namespace_type: 'single',
type: ExceptionListTypeEnum.DETECTION,
};
const defaultExceptionList: List = {
id: '2',
list_id: '123',
namespace_type: 'single',
type: ExceptionListTypeEnum.RULE_DEFAULT,
};
describe('validateRulesWithDuplicatedDefaultExceptionsList.test', () => {
it('is valid array if there no rules', () => {
const result = validateRulesWithDuplicatedDefaultExceptionsList({
ruleId: undefined,
exceptionsList: undefined,
allRules: [],
});
expect(result).toBeUndefined();
});
it('is valid if there no default exceptions list duplicated', () => {
const result = validateRulesWithDuplicatedDefaultExceptionsList({
ruleId: undefined,
exceptionsList: [defaultExceptionList],
allRules: [
{
exceptions_list: [notDefaultExceptionList],
},
{
exceptions_list: [defaultExceptionList],
},
],
});
expect(result).toBeUndefined();
});
it('throw error if there the same default exceptions list', () => {
expect(() =>
validateRulesWithDuplicatedDefaultExceptionsList({
ruleId: undefined,
exceptionsList: [defaultExceptionList],
allRules: [
{
exceptions_list: [defaultExceptionList],
},
{
exceptions_list: [notDefaultExceptionList],
},
{
exceptions_list: [defaultExceptionList],
},
],
})
).toThrow(`default exceptions list 2 is duplicated`);
});
it('throw error with ruleId if there the same default exceptions list', () => {
expect(() =>
validateRulesWithDuplicatedDefaultExceptionsList({
ruleId: '1',
exceptionsList: [defaultExceptionList],
allRules: [
{
exceptions_list: [defaultExceptionList],
},
{
exceptions_list: [notDefaultExceptionList],
},
{
exceptions_list: [defaultExceptionList],
},
],
})
).toThrow(`default exceptions list 2 for rule 1 is duplicated`);
});
});

View file

@ -1,52 +0,0 @@
/*
* 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 { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import type { ListArray } from '@kbn/securitysolution-io-ts-list-types';
import type {
BulkCreateRulesRequestBody,
BulkPatchRulesRequestBody,
BulkUpdateRulesRequestBody,
} from '../../../../../../common/api/detection_engine/rule_management';
import { CustomHttpRequestError } from '../../../../../utils/custom_http_request_error';
/**
* Check if rule has duplicated default exceptions lits
*/
export const validateRulesWithDuplicatedDefaultExceptionsList = ({
exceptionsList,
allRules,
ruleId,
}: {
allRules: BulkCreateRulesRequestBody | BulkPatchRulesRequestBody | BulkUpdateRulesRequestBody;
exceptionsList: ListArray | undefined;
ruleId: string | undefined;
}): void => {
if (!exceptionsList) return;
const defaultExceptionToTuRulesMap: { [key: string]: number[] } = {};
allRules.forEach((rule, ruleIndex) => {
rule.exceptions_list?.forEach((list) => {
if (list.type === ExceptionListTypeEnum.RULE_DEFAULT) {
defaultExceptionToTuRulesMap[list.id] ??= [];
defaultExceptionToTuRulesMap[list.id].push(ruleIndex);
}
});
});
const duplicatedExceptionsList =
exceptionsList
?.filter((list) => list.type === ExceptionListTypeEnum.RULE_DEFAULT)
?.filter((list) => defaultExceptionToTuRulesMap[list.id]?.length > 1) ?? [];
if (duplicatedExceptionsList.length > 0) {
const ids = duplicatedExceptionsList?.map((list) => list.id).join(', ');
throw new CustomHttpRequestError(
`default exceptions list ${ids}${ruleId ? ` for rule ${ruleId}` : ''} is duplicated`,
409
);
}
};

View file

@ -21,11 +21,6 @@ import {
import { replaceParams } from '@kbn/openapi-common/shared';
import { AlertsMigrationCleanupRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/delete_signals_migration/delete_signals_migration.gen';
import { BulkCreateRulesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_create_rules/bulk_create_rules_route.gen';
import { BulkDeleteRulesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_delete_rules/bulk_delete_rules_route.gen';
import { BulkDeleteRulesPostRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_delete_rules/bulk_delete_rules_route.gen';
import { BulkPatchRulesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_patch_rules/bulk_patch_rules_route.gen';
import { BulkUpdateRulesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_update_rules/bulk_update_rules_route.gen';
import { BulkUpsertAssetCriticalityRecordsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/bulk_upload_asset_criticality.gen';
import { CleanDraftTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/clean_draft_timelines/clean_draft_timelines_route.gen';
import { ConfigureRiskEngineSavedObjectRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/engine_configure_saved_object_route.gen';
@ -218,64 +213,6 @@ after 30 days. It also deletes other artifacts specific to the migration impleme
.set(ELASTIC_HTTP_VERSION_HEADER, '1')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana');
},
/**
* Create new detection rules in bulk.
*/
bulkCreateRules(props: BulkCreateRulesProps, kibanaSpace: string = 'default') {
return supertest
.post(routeWithNamespace('/api/detection_engine/rules/_bulk_create', kibanaSpace))
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(props.body as object);
},
/**
* Delete detection rules in bulk.
*/
bulkDeleteRules(props: BulkDeleteRulesProps, kibanaSpace: string = 'default') {
return supertest
.delete(routeWithNamespace('/api/detection_engine/rules/_bulk_delete', kibanaSpace))
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(props.body as object);
},
/**
* Deletes multiple rules.
*/
bulkDeleteRulesPost(props: BulkDeleteRulesPostProps, kibanaSpace: string = 'default') {
return supertest
.post(routeWithNamespace('/api/detection_engine/rules/_bulk_delete', kibanaSpace))
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(props.body as object);
},
/**
* Update specific fields of existing detection rules using the `rule_id` or `id` field.
*/
bulkPatchRules(props: BulkPatchRulesProps, kibanaSpace: string = 'default') {
return supertest
.patch(routeWithNamespace('/api/detection_engine/rules/_bulk_update', kibanaSpace))
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(props.body as object);
},
/**
* Update multiple detection rules using the `rule_id` or `id` field. The original rules are replaced, and all unspecified fields are deleted.
> info
> You cannot modify the `id` or `rule_id` values.
*/
bulkUpdateRules(props: BulkUpdateRulesProps, kibanaSpace: string = 'default') {
return supertest
.put(routeWithNamespace('/api/detection_engine/rules/_bulk_update', kibanaSpace))
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send(props.body as object);
},
/**
* Bulk upsert up to 1000 asset criticality records.
@ -1644,21 +1581,6 @@ detection engine rules.
export interface AlertsMigrationCleanupProps {
body: AlertsMigrationCleanupRequestBodyInput;
}
export interface BulkCreateRulesProps {
body: BulkCreateRulesRequestBodyInput;
}
export interface BulkDeleteRulesProps {
body: BulkDeleteRulesRequestBodyInput;
}
export interface BulkDeleteRulesPostProps {
body: BulkDeleteRulesPostRequestBodyInput;
}
export interface BulkPatchRulesProps {
body: BulkPatchRulesRequestBodyInput;
}
export interface BulkUpdateRulesProps {
body: BulkUpdateRulesRequestBodyInput;
}
export interface BulkUpsertAssetCriticalityRecordsProps {
body: BulkUpsertAssetCriticalityRecordsRequestBodyInput;
}

View file

@ -1,159 +0,0 @@
/*
* 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 expect from 'expect';
import { EsArchivePathBuilder } from '../../../../../es_archive_path_builder';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
import {
getCustomQueryRuleParams,
getSimpleRule,
getSimpleRuleOutput,
getSimpleRuleOutputWithoutRuleId,
getSimpleRuleWithoutRuleId,
removeServerGeneratedProperties,
removeServerGeneratedPropertiesIncludingRuleId,
updateUsername,
} from '../../../utils';
import {
createAlertsIndex,
deleteAllRules,
deleteAllAlerts,
} from '../../../../../../common/utils/security_solution';
export default ({ getService }: FtrProviderContext): void => {
const esArchiver = getService('esArchiver');
const supertest = getService('supertest');
const securitySolutionApi = getService('securitySolutionApi');
const log = getService('log');
const es = getService('es');
// TODO: add a new service for loading archiver files similar to "getService('es')"
const config = getService('config');
const isServerless = config.get('serverless');
const dataPathBuilder = new EsArchivePathBuilder(isServerless);
const auditbeatPath = dataPathBuilder.getPath('auditbeat/hosts');
const utils = getService('securitySolutionUtils');
// TODO: https://github.com/elastic/kibana/issues/193184 Delete this file and clean up the code
describe.skip('@ess @serverless @skipInServerlessMKI create_rules_bulk', () => {
describe('creating rules in bulk', () => {
before(async () => {
await esArchiver.load(auditbeatPath);
});
after(async () => {
await esArchiver.unload(auditbeatPath);
});
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
afterEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
});
it('should create a single rule with a rule_id', async () => {
const { body } = await securitySolutionApi
.bulkCreateRules({ body: [getSimpleRule()] })
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body[0]);
const expectedRule = updateUsername(getSimpleRuleOutput(), await utils.getUsername());
expect(bodyToCompare).toEqual(expectedRule);
});
it('should create a rule with defaultable fields', async () => {
const ruleCreateProperties = getCustomQueryRuleParams({
rule_id: 'rule-1',
max_signals: 200,
setup: '# some setup markdown',
related_integrations: [
{ package: 'package-a', version: '^1.2.3' },
{ package: 'package-b', integration: 'integration-b', version: '~1.1.1' },
],
required_fields: [
{ name: '@timestamp', type: 'date' },
{ name: 'my-non-ecs-field', type: 'keyword' },
],
});
const expectedRule = {
...ruleCreateProperties,
required_fields: [
{ name: '@timestamp', type: 'date', ecs: true },
{ name: 'my-non-ecs-field', type: 'keyword', ecs: false },
],
};
const { body: createdRulesBulkResponse } = await securitySolutionApi
.bulkCreateRules({ body: [ruleCreateProperties] })
.expect(200);
expect(createdRulesBulkResponse[0]).toMatchObject(expectedRule);
const { body: createdRule } = await securitySolutionApi
.readRule({
query: { rule_id: 'rule-1' },
})
.expect(200);
expect(createdRule).toMatchObject(expectedRule);
});
it('should create a single rule without a rule_id', async () => {
const { body } = await securitySolutionApi
.bulkCreateRules({ body: [getSimpleRuleWithoutRuleId()] })
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const expectedRule = updateUsername(
getSimpleRuleOutputWithoutRuleId(),
await utils.getUsername()
);
expect(bodyToCompare).toEqual(expectedRule);
});
it('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id twice', async () => {
const { body } = await securitySolutionApi
.bulkCreateRules({ body: [getSimpleRule(), getSimpleRule()] })
.expect(200);
expect(body).toEqual([
{
error: {
message: 'rule_id: "rule-1" already exists',
status_code: 409,
},
rule_id: 'rule-1',
},
]);
});
it('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id that already exists', async () => {
await securitySolutionApi.bulkCreateRules({ body: [getSimpleRule()] }).expect(200);
const { body } = await securitySolutionApi
.bulkCreateRules({ body: [getSimpleRule()] })
.expect(200);
expect(body).toEqual([
{
error: {
message: 'rule_id: "rule-1" already exists',
status_code: 409,
},
rule_id: 'rule-1',
},
]);
});
});
});
};

View file

@ -1,456 +0,0 @@
/*
* 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 expect from '@kbn/expect';
import {
DETECTION_ENGINE_RULES_BULK_CREATE,
DETECTION_ENGINE_RULES_URL,
NOTIFICATION_DEFAULT_FREQUENCY,
NOTIFICATION_THROTTLE_NO_ACTIONS,
NOTIFICATION_THROTTLE_RULE,
} from '@kbn/security-solution-plugin/common/constants';
import { RuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import {
getSimpleRule,
getSimpleRuleOutput,
getSimpleRuleOutputWithoutRuleId,
getSimpleRuleWithoutRuleId,
removeServerGeneratedProperties,
removeServerGeneratedPropertiesIncludingRuleId,
getActionsWithFrequencies,
getActionsWithoutFrequencies,
getSomeActionsWithFrequencies,
removeUUIDFromActions,
} from '../../../utils';
import {
createAlertsIndex,
deleteAllRules,
deleteAllAlerts,
getRuleForAlertTesting,
waitForRuleSuccess,
} from '../../../../../../common/utils/security_solution';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const log = getService('log');
const es = getService('es');
// TODO: https://github.com/elastic/kibana/issues/193184 Delete this file and clean up the code
describe.skip('@ess @skipInServerless create_rules_bulk', () => {
describe('deprecations', () => {
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('should return a warning header', async () => {
const { header } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_CREATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([getSimpleRule()])
.expect(200);
expect(header.warning).to.be(
'299 Kibana "Deprecated endpoint: /api/detection_engine/rules/_bulk_create API is deprecated since v8.2. Please use the /api/detection_engine/rules/_bulk_action API instead. See https://www.elastic.co/guide/en/security/master/rule-api-overview.html for more detail."'
);
});
});
describe('creating rules in bulk', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts');
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts');
});
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
afterEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
});
it('should create a single rule with a rule_id', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_CREATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([getSimpleRule()])
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).to.eql(getSimpleRuleOutput());
});
/*
This test is to ensure no future regressions introduced by the following scenario
a call to updateApiKey was invalidating the api key used by the
rule while the rule was executing, or even before it executed,
on the first rule run.
this pr https://github.com/elastic/kibana/pull/68184
fixed this by finding the true source of a bug that required the manual
api key update, and removed the call to that function.
When the api key is updated before / while the rule is executing, the alert
executor no longer has access to a service to update the rule status
saved object in Elasticsearch. Because of this, we cannot set the rule into
a 'failure' state, so the user ends up seeing 'running' as that is the
last status set for the rule before it erupts in an error that cannot be
recorded inside of the executor.
This adds an e2e test for the backend to catch that in case
this pops up again elsewhere.
*/
it('should create a single rule with a rule_id and validate it ran successfully', async () => {
const rule = {
...getRuleForAlertTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_CREATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([rule])
.expect(200);
await waitForRuleSuccess({ supertest, log, id: body[0].id });
});
it('should create a single rule without a rule_id', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_CREATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([getSimpleRuleWithoutRuleId()])
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId());
});
it('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id twice', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_CREATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([getSimpleRule(), getSimpleRule()])
.expect(200);
expect(body).to.eql([
{
error: {
message: 'rule_id: "rule-1" already exists',
status_code: 409,
},
rule_id: 'rule-1',
},
]);
});
it('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id that already exists', async () => {
await supertest
.post(DETECTION_ENGINE_RULES_BULK_CREATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([getSimpleRule()])
.expect(200);
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_CREATE)
.set('kbn-xsrf', 'foo')
.set('elastic-api-version', '2023-10-31')
.send([getSimpleRule()])
.expect(200);
expect(body).to.eql([
{
error: {
message: 'rule_id: "rule-1" already exists',
status_code: 409,
},
rule_id: 'rule-1',
},
]);
});
it('should return a 200 ok but have a 409 conflict if we attempt to create the rule, which use existing attached rule default list', async () => {
const { body: ruleWithException } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send({
...getSimpleRuleWithoutRuleId(),
exceptions_list: [
{
id: '2',
list_id: '123',
namespace_type: 'single',
type: ExceptionListTypeEnum.RULE_DEFAULT,
},
],
})
.expect(200);
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_CREATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([
{
...getSimpleRule(),
exceptions_list: [
{
id: '2',
list_id: '123',
namespace_type: 'single',
type: ExceptionListTypeEnum.RULE_DEFAULT,
},
],
},
])
.expect(200);
expect(body).to.eql([
{
error: {
message: `default exception list for rule: rule-1 already exists in rule(s): ${ruleWithException.id}`,
status_code: 409,
},
rule_id: 'rule-1',
},
]);
});
it('should return a 409 if several rules has the same exception rule default list', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_CREATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([
{
...getSimpleRule(),
exceptions_list: [
{
id: '2',
list_id: '123',
namespace_type: 'single',
type: ExceptionListTypeEnum.RULE_DEFAULT,
},
],
},
{
...getSimpleRule('rule-2'),
exceptions_list: [
{
id: '2',
list_id: '123',
namespace_type: 'single',
type: ExceptionListTypeEnum.RULE_DEFAULT,
},
],
},
{
...getSimpleRuleWithoutRuleId(),
exceptions_list: [
{
id: '2',
list_id: '123',
namespace_type: 'single',
type: ExceptionListTypeEnum.RULE_DEFAULT,
},
],
},
])
.expect(200);
expect(body).to.eql([
{
error: {
message: 'default exceptions list 2 for rule rule-1 is duplicated',
status_code: 409,
},
rule_id: 'rule-1',
},
{
error: {
message: 'default exceptions list 2 for rule rule-2 is duplicated',
status_code: 409,
},
rule_id: 'rule-2',
},
{
error: {
message: 'default exceptions list 2 is duplicated',
status_code: 409,
},
rule_id: '(unknown id)',
},
]);
});
describe('per-action frequencies', () => {
const bulkCreateSingleRule = async (rule: RuleCreateProps) => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_CREATE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([rule])
.expect(200);
const createdRule = body[0];
createdRule.actions = removeUUIDFromActions(createdRule.actions);
return removeServerGeneratedPropertiesIncludingRuleId(createdRule);
};
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);
const simpleRule = getSimpleRuleWithoutRuleId();
simpleRule.throttle = throttle;
simpleRule.actions = actionsWithoutFrequencies;
const createdRule = await bulkCreateSingleRule(simpleRule);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.actions = actionsWithoutFrequencies.map((action) => ({
...action,
frequency: NOTIFICATION_DEFAULT_FREQUENCY,
}));
expect(createdRule).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);
const simpleRule = getSimpleRuleWithoutRuleId();
simpleRule.throttle = throttle;
simpleRule.actions = actionsWithoutFrequencies;
const createdRule = await bulkCreateSingleRule(simpleRule);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.actions = actionsWithoutFrequencies.map((action) => ({
...action,
frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' },
}));
expect(createdRule).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);
const simpleRule = getSimpleRuleWithoutRuleId();
simpleRule.throttle = throttle;
simpleRule.actions = actionsWithFrequencies;
const createdRule = await bulkCreateSingleRule(simpleRule);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.actions = actionsWithFrequencies;
expect(createdRule).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);
const simpleRule = getSimpleRuleWithoutRuleId();
simpleRule.throttle = throttle;
simpleRule.actions = someActionsWithFrequencies;
const createdRule = await bulkCreateSingleRule(simpleRule);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.actions = someActionsWithFrequencies.map((action) => ({
...action,
frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY,
}));
expect(createdRule).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);
const simpleRule = getSimpleRuleWithoutRuleId();
simpleRule.throttle = throttle;
simpleRule.actions = someActionsWithFrequencies;
const createdRule = await bulkCreateSingleRule(simpleRule);
const expectedRule = getSimpleRuleOutputWithoutRuleId();
expectedRule.actions = someActionsWithFrequencies.map((action) => ({
...action,
frequency: action.frequency ?? {
summary: true,
throttle,
notifyWhen: 'onThrottleInterval',
},
}));
expect(createdRule).to.eql(expectedRule);
});
});
});
});
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]: 0.investigation_fields: Expected object, received array'
);
});
});
});
});
};

View file

@ -7,14 +7,10 @@
import expect from '@kbn/expect';
import { DETECTION_ENGINE_RULES_BULK_DELETE } from '@kbn/security-solution-plugin/common/constants';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
import {
getSimpleRule,
getSimpleRuleOutput,
getSimpleRuleOutputWithoutRuleId,
getSimpleRuleWithoutRuleId,
removeServerGeneratedProperties,
removeServerGeneratedPropertiesIncludingRuleId,
updateUsername,
} from '../../../utils';
@ -32,9 +28,8 @@ export default ({ getService }: FtrProviderContext): void => {
const es = getService('es');
const utils = getService('securitySolutionUtils');
// TODO: https://github.com/elastic/kibana/issues/193184 Unskip and rewrite using the _bulk_action API endpoint
describe.skip('@ess @serverless @skipInServerlessMKI delete_rules_bulk', () => {
describe('deleting rules bulk using DELETE', () => {
describe('@ess @serverless @skipInServerlessMKI bulk_actions delete', () => {
describe('deleting rules using bulk_actions delete', () => {
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
@ -44,154 +39,20 @@ export default ({ getService }: FtrProviderContext): void => {
await deleteAllRules(supertest, log);
});
it('should delete a single rule with a rule_id', async () => {
await createRule(supertest, log, getSimpleRule());
// delete the rule in bulk
const { body } = await securitySolutionApi
.bulkDeleteRules({ body: [{ rule_id: 'rule-1' }] })
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body[0]);
const expectedRule = updateUsername(getSimpleRuleOutput(), await utils.getUsername());
expect(bodyToCompare).to.eql(expectedRule);
});
it('should delete a single rule using an auto generated rule_id', async () => {
const bodyWithCreatedRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
// delete that rule by its rule_id
const { body } = await securitySolutionApi
.bulkDeleteRules({ body: [{ rule_id: bodyWithCreatedRule.rule_id }] })
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const expectedRule = updateUsername(
getSimpleRuleOutputWithoutRuleId(),
await utils.getUsername()
);
expect(bodyToCompare).to.eql(expectedRule);
});
it('should delete a single rule using an auto generated id', async () => {
const bodyWithCreatedRule = await createRule(supertest, log, getSimpleRule());
// delete that rule by its id
// delete the rule in bulk using the bulk_actions endpoint
const { body } = await securitySolutionApi
.bulkDeleteRules({ body: [{ id: bodyWithCreatedRule.id }] })
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const expectedRule = updateUsername(
getSimpleRuleOutputWithoutRuleId(),
await utils.getUsername()
);
expect(bodyToCompare).to.eql(expectedRule);
});
it('should return an error if the ruled_id does not exist when trying to delete a rule_id', async () => {
const { body } = await securitySolutionApi
.bulkDeleteRules({ body: [{ rule_id: 'fake_id' }] })
.expect(200);
expect(body).to.eql([
{
error: {
message: 'rule_id: "fake_id" not found',
status_code: 404,
},
rule_id: 'fake_id',
},
]);
});
it('should return an error if the id does not exist when trying to delete an id', async () => {
const { body } = await securitySolutionApi
.bulkDeleteRules({ body: [{ id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }] })
.expect(200);
expect(body).to.eql([
{
error: {
message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found',
status_code: 404,
},
id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612',
},
]);
});
it('should delete a single rule using an auto generated rule_id but give an error if the second rule does not exist', async () => {
const bodyWithCreatedRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
const { body } = await securitySolutionApi
.bulkDeleteRules({
body: [{ id: bodyWithCreatedRule.id }, { id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }],
.performRulesBulkAction({
query: { dry_run: false },
body: { ids: [bodyWithCreatedRule.id], action: 'delete' },
})
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const expectedRule = updateUsername(
getSimpleRuleOutputWithoutRuleId(),
await utils.getUsername()
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(
body.attributes.results.deleted[0]
);
expect([bodyToCompare, body[1]]).to.eql([
expectedRule,
{
id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612',
error: {
status_code: 404,
message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found',
},
},
]);
});
});
// This is a repeat of the tests above but just using POST instead of DELETE
describe('deleting rules bulk using POST', () => {
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
afterEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
});
it('should delete a single rule with a rule_id', async () => {
await createRule(supertest, log, getSimpleRule());
// 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-1' }])
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body[0]);
const expectedRule = updateUsername(getSimpleRuleOutput(), await utils.getUsername());
expect(bodyToCompare).to.eql(expectedRule);
});
it('should delete a single rule using an auto generated rule_id', async () => {
const bodyWithCreatedRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
// delete that rule by its rule_id
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_DELETE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([{ rule_id: bodyWithCreatedRule.rule_id }])
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const expectedRule = updateUsername(
getSimpleRuleOutputWithoutRuleId(),
await utils.getUsername()
@ -200,90 +61,44 @@ export default ({ getService }: FtrProviderContext): void => {
expect(bodyToCompare).to.eql(expectedRule);
});
it('should delete a single rule using an auto generated id', async () => {
const bodyWithCreatedRule = await createRule(supertest, log, getSimpleRule());
// delete that rule by its id
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_DELETE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([{ id: bodyWithCreatedRule.id }])
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const expectedRule = updateUsername(
getSimpleRuleOutputWithoutRuleId(),
await utils.getUsername()
);
expect(bodyToCompare).to.eql(expectedRule);
});
it('should return an error if the ruled_id does not exist when trying to delete a rule_id', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_DELETE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([{ rule_id: 'fake_id' }])
.expect(200);
expect(body).to.eql([
{
error: {
message: 'rule_id: "fake_id" not found',
status_code: 404,
},
rule_id: 'fake_id',
},
]);
});
it('should return an error if the id does not exist when trying to delete an id', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_DELETE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([{ id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }])
.expect(200);
const { body } = await securitySolutionApi
.performRulesBulkAction({
query: { dry_run: false },
body: { ids: ['c4e80a0d-e20f-4efc-84c1-08112da5a612'], action: 'delete' },
})
.expect(500);
expect(body).to.eql([
{
error: {
message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found',
status_code: 404,
expect(body).to.eql({
statusCode: 500,
error: 'Internal Server Error',
message: 'Bulk edit failed',
attributes: {
errors: [
{
message: 'Rule not found',
status_code: 500,
rules: [
{
id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612',
},
],
},
],
results: {
updated: [],
created: [],
deleted: [],
skipped: [],
},
id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612',
},
]);
});
it('should delete a single rule using an auto generated rule_id but give an error if the second rule does not exist', async () => {
const bodyWithCreatedRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_DELETE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([{ id: bodyWithCreatedRule.id }, { id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }])
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const expectedRule = updateUsername(
getSimpleRuleOutputWithoutRuleId(),
await utils.getUsername()
);
expect([bodyToCompare, body[1]]).to.eql([
expectedRule,
{
id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612',
error: {
status_code: 404,
message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found',
summary: {
failed: 1,
succeeded: 0,
skipped: 0,
total: 1,
},
},
]);
});
});
});
});

View file

@ -8,19 +8,15 @@
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_BULK_DELETE } from '@kbn/security-solution-plugin/common/constants';
import { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine';
import {
getSimpleRule,
getSimpleRuleOutput,
updateUsername,
getSimpleRuleOutputWithoutRuleId,
getSimpleRuleWithoutRuleId,
removeServerGeneratedProperties,
removeServerGeneratedPropertiesIncludingRuleId,
createRuleThroughAlertingEndpoint,
getRuleSavedObjectWithLegacyInvestigationFields,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
removeServerGeneratedPropertiesIncludingRuleId,
updateUsername,
getSimpleRuleOutputWithoutRuleId,
} from '../../../utils';
import {
createRule,
@ -37,23 +33,8 @@ export default ({ getService }: FtrProviderContext): void => {
const es = getService('es');
const utils = getService('securitySolutionUtils');
// TODO: https://github.com/elastic/kibana/issues/193184 Unskip and rewrite using the _bulk_action API endpoint
describe.skip('@ess @skipInServerlesMKI delete_rules_bulk', () => {
describe('deprecations', () => {
it('should return a warning header', async () => {
await createRule(supertest, log, getSimpleRule());
const { header } = await securitySolutionApi
.bulkDeleteRules({ body: [{ rule_id: 'rule-1' }] })
.expect(200);
expect(header.warning).to.be(
'299 Kibana "Deprecated endpoint: /api/detection_engine/rules/_bulk_delete API is deprecated since v8.2. Please use the /api/detection_engine/rules/_bulk_action API instead. See https://www.elastic.co/guide/en/security/master/rule-api-overview.html for more detail."'
);
});
});
describe('deleting rules bulk using DELETE', () => {
describe('@ess @skipInServerlesMKI delete_rules_bulk', () => {
describe('deleting rules bulk using bulk_action endpoint', () => {
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
@ -63,153 +44,20 @@ export default ({ getService }: FtrProviderContext): void => {
await deleteAllRules(supertest, log);
});
it('should delete a single rule with a rule_id', async () => {
await createRule(supertest, log, getSimpleRule());
// delete the rule in bulk
const { body } = await securitySolutionApi
.bulkDeleteRules({ body: [{ rule_id: 'rule-1' }] })
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body[0]);
const expectedRule = updateUsername(getSimpleRuleOutput(), await utils.getUsername());
expect(bodyToCompare).to.eql(expectedRule);
});
it('should delete a single rule using an auto generated rule_id', async () => {
const bodyWithCreatedRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
// delete that rule by its rule_id
const { body } = await securitySolutionApi
.bulkDeleteRules({ body: [{ rule_id: bodyWithCreatedRule.rule_id }] })
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const expectedRule = updateUsername(
getSimpleRuleOutputWithoutRuleId(),
await utils.getUsername()
);
expect(bodyToCompare).to.eql(expectedRule);
});
it('should delete a single rule using an auto generated id', async () => {
const bodyWithCreatedRule = await createRule(supertest, log, getSimpleRule());
// delete that rule by its id
const { body } = await securitySolutionApi
.bulkDeleteRules({ body: [{ id: bodyWithCreatedRule.id }] })
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const expectedRule = updateUsername(
getSimpleRuleOutputWithoutRuleId(),
await utils.getUsername()
);
expect(bodyToCompare).to.eql(expectedRule);
});
it('should return an error if the ruled_id does not exist when trying to delete a rule_id', async () => {
const { body } = await securitySolutionApi
.bulkDeleteRules({ body: [{ rule_id: 'fake_id' }] })
.expect(200);
expect(body).to.eql([
{
error: {
message: 'rule_id: "fake_id" not found',
status_code: 404,
},
rule_id: 'fake_id',
},
]);
});
it('should return an error if the id does not exist when trying to delete an id', async () => {
const { body } = await securitySolutionApi
.bulkDeleteRules({ body: [{ id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }] })
.expect(200);
expect(body).to.eql([
{
error: {
message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found',
status_code: 404,
},
id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612',
},
]);
});
it('should delete a single rule using an auto generated rule_id but give an error if the second rule does not exist', async () => {
const bodyWithCreatedRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
const { body } = await securitySolutionApi
.bulkDeleteRules({
body: [{ id: bodyWithCreatedRule.id }, { id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }],
.performRulesBulkAction({
query: { dry_run: false },
body: { ids: [bodyWithCreatedRule.id], action: 'delete' },
})
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const expectedRule = updateUsername(
getSimpleRuleOutputWithoutRuleId(),
await utils.getUsername()
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(
body.attributes.results.deleted[0]
);
expect([bodyToCompare, body[1]]).to.eql([
expectedRule,
{
id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612',
error: {
status_code: 404,
message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found',
},
},
]);
});
});
// This is a repeat of the tests above but just using POST instead of DELETE
describe('deleting rules bulk using POST', () => {
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
afterEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
});
it('should delete a single rule with a rule_id', async () => {
await createRule(supertest, log, getSimpleRule());
// 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-1' }])
.expect(200);
const bodyToCompare = removeServerGeneratedProperties(body[0]);
const expectedRule = updateUsername(getSimpleRuleOutput(), await utils.getUsername());
expect(bodyToCompare).to.eql(expectedRule);
});
it('should delete a single rule using an auto generated rule_id', async () => {
const bodyWithCreatedRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
// delete that rule by its rule_id
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_DELETE)
.send([{ rule_id: bodyWithCreatedRule.rule_id }])
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const expectedRule = updateUsername(
getSimpleRuleOutputWithoutRuleId(),
await utils.getUsername()
@ -218,90 +66,44 @@ export default ({ getService }: FtrProviderContext): void => {
expect(bodyToCompare).to.eql(expectedRule);
});
it('should delete a single rule using an auto generated id', async () => {
const bodyWithCreatedRule = await createRule(supertest, log, getSimpleRule());
// delete that rule by its id
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_DELETE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([{ id: bodyWithCreatedRule.id }])
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const expectedRule = updateUsername(
getSimpleRuleOutputWithoutRuleId(),
await utils.getUsername()
);
expect(bodyToCompare).to.eql(expectedRule);
});
it('should return an error if the ruled_id does not exist when trying to delete a rule_id', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_DELETE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([{ rule_id: 'fake_id' }])
.expect(200);
expect(body).to.eql([
{
error: {
message: 'rule_id: "fake_id" not found',
status_code: 404,
},
rule_id: 'fake_id',
},
]);
});
it('should return an error if the id does not exist when trying to delete an id', async () => {
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_DELETE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([{ id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }])
.expect(200);
const { body } = await securitySolutionApi
.performRulesBulkAction({
query: { dry_run: false },
body: { ids: ['c4e80a0d-e20f-4efc-84c1-08112da5a612'], action: 'delete' },
})
.expect(500);
expect(body).to.eql([
{
error: {
message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found',
status_code: 404,
expect(body).to.eql({
statusCode: 500,
error: 'Internal Server Error',
message: 'Bulk edit failed',
attributes: {
errors: [
{
message: 'Rule not found',
status_code: 500,
rules: [
{
id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612',
},
],
},
],
results: {
updated: [],
created: [],
deleted: [],
skipped: [],
},
id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612',
},
]);
});
it('should delete a single rule using an auto generated rule_id but give an error if the second rule does not exist', async () => {
const bodyWithCreatedRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId());
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_DELETE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([{ id: bodyWithCreatedRule.id }, { id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612' }])
.expect(200);
const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const expectedRule = updateUsername(
getSimpleRuleOutputWithoutRuleId(),
await utils.getUsername()
);
expect([bodyToCompare, body[1]]).to.eql([
expectedRule,
{
id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612',
error: {
status_code: 404,
message: 'id: "c4e80a0d-e20f-4efc-84c1-08112da5a612" not found',
summary: {
failed: 1,
succeeded: 0,
skipped: 0,
total: 1,
},
},
]);
});
});
});
@ -321,53 +123,37 @@ export default ({ getService }: FtrProviderContext): void => {
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
it('Should delete a single rule with investigation field', async () => {
// delete the rule in bulk using the bulk_actions endpoint
const { body } = await securitySolutionApi
.bulkDeleteRules({
body: [
{ rule_id: 'rule-with-investigation-field' },
{ rule_id: ruleWithLegacyInvestigationFieldEmptyArray.params.ruleId },
{ rule_id: ruleWithLegacyInvestigationField.params.ruleId },
],
.performRulesBulkAction({
query: { dry_run: false },
body: {
ids: [
ruleWithLegacyInvestigationFieldEmptyArray.id,
ruleWithLegacyInvestigationField.id,
],
action: 'delete',
},
})
.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(body.success).to.be(true);
expect(body.rules_count).to.be(2);
const investigationFields = body.attributes.results.deleted
.map((rule: RuleResponse) => rule.investigation_fields)
.sort();
expect(investigationFields).to.eql([
{ field_names: ['host.name'] },
undefined,
{ field_names: ['client.address', 'agent.name'] },
undefined,
]);
});
});

View file

@ -7,7 +7,6 @@
import expect from '@kbn/expect';
import { BASE_ALERTING_API_PATH } from '@kbn/alerting-plugin/common';
import { DETECTION_ENGINE_RULES_BULK_DELETE } from '@kbn/security-solution-plugin/common/constants';
import {
createLegacyRuleAction,
getSimpleRule,
@ -25,12 +24,12 @@ import { FtrProviderContext } from '../../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest');
const securitySolutionApi = getService('securitySolutionApi');
const log = getService('log');
const es = getService('es');
// TODO: https://github.com/elastic/kibana/issues/193184 Unskip and rewrite using the _bulk_action API endpoint
describe.skip('@ess delete_rules_bulk_legacy', () => {
describe('deleting rules bulk using POST', () => {
describe('@ess delete_rules_bulk_legacy', () => {
describe('deleting rules bulk using bulk_action endpoint', () => {
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
@ -57,19 +56,20 @@ export default ({ getService }: FtrProviderContext): void => {
// Add a legacy rule action to the body of the rule
await createLegacyRuleAction(supertest, createRuleBody.id, hookAction.id);
// delete the rule with the legacy action
const { body } = await supertest
.delete(DETECTION_ENGINE_RULES_BULK_DELETE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([{ id: createRuleBody.id }])
// delete the rule in bulk using the bulk_actions endpoint
const { body } = await securitySolutionApi
.performRulesBulkAction({
query: { dry_run: false },
body: {
ids: [createRuleBody.id],
action: 'delete',
},
})
.expect(200);
// ensure we only get one body back
expect(body.length).to.eql(1);
expect(body.attributes.results.deleted.length).to.eql(1);
// ensure that its actions equal what we expect
expect(body[0].actions).to.eql([
expect(body.attributes.results.deleted[0].actions).to.eql([
{
id: hookAction.id,
action_type_id: hookAction.connector_type_id,
@ -107,19 +107,22 @@ export default ({ getService }: FtrProviderContext): void => {
await createLegacyRuleAction(supertest, createRuleBody1.id, hookAction1.id);
await createLegacyRuleAction(supertest, createRuleBody2.id, hookAction2.id);
// delete 2 rules where both have legacy actions
const { body } = await supertest
.delete(DETECTION_ENGINE_RULES_BULK_DELETE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([{ id: createRuleBody1.id }, { id: createRuleBody2.id }])
// delete the rule in bulk using the bulk_actions endpoint
const { body } = await securitySolutionApi
.performRulesBulkAction({
query: { dry_run: false },
body: {
ids: [createRuleBody1.id, createRuleBody2.id],
action: 'delete',
},
})
.expect(200);
// ensure we only get two bodies back
expect(body.length).to.eql(2);
expect(body.attributes.results.deleted.length).to.eql(2);
// ensure that its actions equal what we expect for both responses
expect(body[0].actions).to.eql([
expect(body.attributes.results.deleted[0].actions).to.eql([
{
id: hookAction1.id,
action_type_id: hookAction1.connector_type_id,
@ -131,7 +134,7 @@ export default ({ getService }: FtrProviderContext): void => {
frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' },
},
]);
expect(body[1].actions).to.eql([
expect(body.attributes.results.deleted[1].actions).to.eql([
{
id: hookAction2.id,
action_type_id: hookAction2.connector_type_id,
@ -169,12 +172,15 @@ export default ({ getService }: FtrProviderContext): void => {
createRuleBody.id
);
// bulk delete the rule
await supertest
.delete(DETECTION_ENGINE_RULES_BULK_DELETE)
.set('kbn-xsrf', 'true')
.set('elastic-api-version', '2023-10-31')
.send([{ id: createRuleBody.id }])
// delete the rule in bulk using the bulk_actions endpoint
await securitySolutionApi
.performRulesBulkAction({
query: { dry_run: false },
body: {
ids: [createRuleBody.id],
action: 'delete',
},
})
.expect(200);
// Test to ensure that we have exactly 0 legacy actions by querying the Alerting client REST API directly

View file

@ -1,392 +0,0 @@
/*
* 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 expect from 'expect';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
import {
getSimpleRule,
getSimpleRuleOutput,
getCustomQueryRuleParams,
removeServerGeneratedProperties,
getSimpleRuleOutputWithoutRuleId,
removeServerGeneratedPropertiesIncludingRuleId,
updateUsername,
createHistoricalPrebuiltRuleAssetSavedObjects,
installPrebuiltRules,
createRuleAssetSavedObject,
} from '../../../utils';
import {
createAlertsIndex,
deleteAllRules,
createRule,
deleteAllAlerts,
} from '../../../../../../common/utils/security_solution';
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const securitySolutionApi = getService('securitySolutionApi');
const log = getService('log');
const es = getService('es');
const utils = getService('securitySolutionUtils');
// TODO: https://github.com/elastic/kibana/issues/193184 Delete this file and clean up the code
describe.skip('@ess @serverless @skipInServerlessMKI patch_rules_bulk', () => {
describe('patch rules bulk', () => {
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
afterEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
});
it('should patch a single rule property of name using a rule_id', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// patch a simple rule's name
const { body } = await securitySolutionApi
.bulkPatchRules({ body: [{ rule_id: 'rule-1', name: 'some other name' }] })
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.name = 'some other name';
outputRule.revision = 1;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
const expectedRule = updateUsername(outputRule, await utils.getUsername());
expect(bodyToCompare).toEqual(expectedRule);
});
it('should patch defaultable fields', async () => {
const rulePatchProperties = getCustomQueryRuleParams({
rule_id: 'rule-1',
max_signals: 200,
setup: '# some setup markdown',
related_integrations: [
{ package: 'package-a', version: '^1.2.3' },
{ package: 'package-b', integration: 'integration-b', version: '~1.1.1' },
],
required_fields: [{ name: '@timestamp', type: 'date' }],
});
const expectedRule = {
...rulePatchProperties,
required_fields: [{ name: '@timestamp', type: 'date', ecs: true }],
};
await securitySolutionApi.createRule({
body: getCustomQueryRuleParams({ rule_id: 'rule-1' }),
});
const { body: patchedRulesBulkResponse } = await securitySolutionApi
.bulkPatchRules({
body: [
{
...rulePatchProperties,
},
],
})
.expect(200);
expect(patchedRulesBulkResponse[0]).toMatchObject(expectedRule);
const { body: patchedRule } = await securitySolutionApi
.readRule({
query: { rule_id: 'rule-1' },
})
.expect(200);
expect(patchedRule).toMatchObject(expectedRule);
});
it('should patch two rule properties of name using the two rules rule_id', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
await createRule(supertest, log, getSimpleRule('rule-2'));
const username = await utils.getUsername();
// patch both rule names
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [
{ rule_id: 'rule-1', name: 'some other name' },
{ rule_id: 'rule-2', name: 'some other name' },
],
})
.expect(200);
const outputRule1 = getSimpleRuleOutput();
outputRule1.name = 'some other name';
outputRule1.revision = 1;
const expectedRule1 = updateUsername(outputRule1, username);
const outputRule2 = getSimpleRuleOutput('rule-2');
outputRule2.name = 'some other name';
outputRule2.revision = 1;
const expectedRule2 = updateUsername(outputRule2, username);
const bodyToCompare1 = removeServerGeneratedProperties(body[0]);
const bodyToCompare2 = removeServerGeneratedProperties(body[1]);
expect(bodyToCompare1).toEqual(expectedRule1);
expect(bodyToCompare2).toEqual(expectedRule2);
});
it('should patch a single rule property of name using an id', async () => {
const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1'));
// patch a simple rule's name
const { body } = await securitySolutionApi
.bulkPatchRules({ body: [{ id: createRuleBody.id, name: 'some other name' }] })
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.name = 'some other name';
outputRule.revision = 1;
const expectedRule = updateUsername(outputRule, await utils.getUsername());
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).toEqual(expectedRule);
});
it('should patch two rule properties of name using the two rules id', async () => {
const createRule1 = await createRule(supertest, log, getSimpleRule('rule-1'));
const createRule2 = await createRule(supertest, log, getSimpleRule('rule-2'));
const username = await utils.getUsername();
// patch both rule names
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [
{ id: createRule1.id, name: 'some other name' },
{ id: createRule2.id, name: 'some other name' },
],
})
.expect(200);
const outputRule1 = getSimpleRuleOutputWithoutRuleId('rule-1');
outputRule1.name = 'some other name';
outputRule1.revision = 1;
const expectedRule = updateUsername(outputRule1, username);
const outputRule2 = getSimpleRuleOutputWithoutRuleId('rule-2');
outputRule2.name = 'some other name';
outputRule2.revision = 1;
const expectedRule2 = updateUsername(outputRule2, username);
const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]);
expect(bodyToCompare1).toEqual(expectedRule);
expect(bodyToCompare2).toEqual(expectedRule2);
});
it('should patch a single rule property of name using the auto-generated id', async () => {
const createdBody = await createRule(supertest, log, getSimpleRule('rule-1'));
// patch a simple rule's name
const { body } = await securitySolutionApi
.bulkPatchRules({ body: [{ id: createdBody.id, name: 'some other name' }] })
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.name = 'some other name';
outputRule.revision = 1;
const expectedRule = updateUsername(outputRule, await utils.getUsername());
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).toEqual(expectedRule);
});
it('should not change the revision of a rule when it patches only enabled', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// patch a simple rule's enabled to false
const { body } = await securitySolutionApi
.bulkPatchRules({ body: [{ rule_id: 'rule-1', enabled: false }] })
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.enabled = false;
const expectedRule = updateUsername(outputRule, await utils.getUsername());
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).toEqual(expectedRule);
});
it('should change the revision of a rule when it patches enabled and another property', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// patch a simple rule's enabled to false and another property
const { body } = await securitySolutionApi
.bulkPatchRules({ body: [{ rule_id: 'rule-1', severity: 'low', enabled: false }] })
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.enabled = false;
outputRule.severity = 'low';
outputRule.revision = 1;
const expectedRule = updateUsername(outputRule, await utils.getUsername());
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).toEqual(expectedRule);
});
it('should not change other properties when it does patches', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// patch a simple rule's timeline_title
await securitySolutionApi
.bulkPatchRules({
body: [{ rule_id: 'rule-1', timeline_title: 'some title', timeline_id: 'some id' }],
})
.expect(200);
// patch a simple rule's name
const { body } = await securitySolutionApi
.bulkPatchRules({ body: [{ rule_id: 'rule-1', name: 'some other name' }] })
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.name = 'some other name';
outputRule.timeline_title = 'some title';
outputRule.timeline_id = 'some id';
outputRule.revision = 2;
const expectedRule = updateUsername(outputRule, await utils.getUsername());
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).toEqual(expectedRule);
});
it('should return a 200 but give a 404 in the message if it is given a fake id', async () => {
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [{ id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', name: 'some other name' }],
})
.expect(200);
expect(body).toEqual([
{
id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d',
error: {
status_code: 404,
message: 'id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" not found',
},
},
]);
});
it('should return a 200 but give a 404 in the message if it is given a fake rule_id', async () => {
const { body } = await securitySolutionApi
.bulkPatchRules({ body: [{ rule_id: 'fake_id', name: 'some other name' }] })
.expect(200);
expect(body).toEqual([
{
rule_id: 'fake_id',
error: { status_code: 404, message: 'rule_id: "fake_id" not found' },
},
]);
});
it('should patch one rule property and give an error about a second fake rule_id', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// patch one rule name and give a fake id for the second
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [
{ rule_id: 'rule-1', name: 'some other name' },
{ rule_id: 'fake_id', name: 'some other name' },
],
})
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.name = 'some other name';
outputRule.revision = 1;
const expectedRule = updateUsername(outputRule, await utils.getUsername());
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect([bodyToCompare, body[1]]).toEqual([
expectedRule,
{
error: {
message: 'rule_id: "fake_id" not found',
status_code: 404,
},
rule_id: 'fake_id',
},
]);
});
it('should patch one rule property and give an error about a second fake id', async () => {
const createdBody = await createRule(supertest, log, getSimpleRule('rule-1'));
// patch one rule name and give a fake id for the second
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [
{ id: createdBody.id, name: 'some other name' },
{ id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', name: 'some other name' },
],
})
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.name = 'some other name';
outputRule.revision = 1;
const expectedRule = updateUsername(outputRule, await utils.getUsername());
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect([bodyToCompare, body[1]]).toEqual([
expectedRule,
{
error: {
message: 'id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" not found',
status_code: 404,
},
id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d',
},
]);
});
// Unskip: https://github.com/elastic/kibana/issues/195921
it('@skipInServerlessMKI throws an error if rule has external rule source and non-customizable fields are changed', async () => {
// Install base prebuilt detection rule
await createHistoricalPrebuiltRuleAssetSavedObjects(es, [
createRuleAssetSavedObject({ rule_id: 'rule-1', author: ['elastic'] }),
createRuleAssetSavedObject({ rule_id: 'rule-2', license: 'basic' }),
]);
await installPrebuiltRules(es, supertest);
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [
{ rule_id: 'rule-1', author: ['new user'] },
{ rule_id: 'rule-2', license: 'new license' },
],
})
.expect(200);
expect([body[0], body[1]]).toEqual([
{
error: {
message: 'Cannot update "author" field for prebuilt rules',
status_code: 400,
},
rule_id: 'rule-1',
},
{
error: {
message: 'Cannot update "license" field for prebuilt rules',
status_code: 400,
},
rule_id: 'rule-2',
},
]);
});
});
});
};

View file

@ -1,628 +0,0 @@
/*
* 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 expect from '@kbn/expect';
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 {
getSimpleRule,
getSimpleRuleOutput,
removeServerGeneratedProperties,
getSimpleRuleOutputWithoutRuleId,
removeServerGeneratedPropertiesIncludingRuleId,
createLegacyRuleAction,
getLegacyActionSO,
getRuleSOById,
updateUsername,
createRuleThroughAlertingEndpoint,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
getRuleSavedObjectWithLegacyInvestigationFields,
checkInvestigationFieldSoValue,
} from '../../../utils';
import {
createAlertsIndex,
deleteAllRules,
deleteAllAlerts,
createRule,
} from '../../../../../../common/utils/security_solution';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const securitySolutionApi = getService('securitySolutionApi');
const log = getService('log');
const es = getService('es');
const utils = getService('securitySolutionUtils');
// TODO: https://github.com/elastic/kibana/issues/193184 Delete this file and clean up the code
describe.skip('@ess @skipInServerless patch_rules_bulk', () => {
describe('deprecations', () => {
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('should return a warning header', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
const { header } = await securitySolutionApi
.bulkPatchRules({ body: [{ rule_id: 'rule-1', name: 'some other name' }] })
.expect(200);
expect(header.warning).to.be(
'299 Kibana "Deprecated endpoint: /api/detection_engine/rules/_bulk_update API is deprecated since v8.2. Please use the /api/detection_engine/rules/_bulk_action API instead. See https://www.elastic.co/guide/en/security/master/rule-api-overview.html for more detail."'
);
});
});
describe('patch rules bulk', () => {
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
afterEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
});
it('should patch a single rule property of name using a rule_id', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// patch a simple rule's name
const { body } = await securitySolutionApi
.bulkPatchRules({ body: [{ rule_id: 'rule-1', name: 'some other name' }] })
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), await utils.getUsername());
outputRule.name = 'some other name';
outputRule.revision = 1;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).to.eql(outputRule);
});
it('should patch two rule properties of name using the two rules rule_id', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
await createRule(supertest, log, getSimpleRule('rule-2'));
const username = await utils.getUsername();
// patch both rule names
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [
{ rule_id: 'rule-1', name: 'some other name' },
{ rule_id: 'rule-2', name: 'some other name' },
],
})
.expect(200);
const outputRule1 = updateUsername(getSimpleRuleOutput('rule-1'), username);
outputRule1.name = 'some other name';
outputRule1.revision = 1;
const outputRule2 = updateUsername(getSimpleRuleOutput('rule-2'), username);
outputRule2.name = 'some other name';
outputRule2.revision = 1;
const bodyToCompare1 = removeServerGeneratedProperties(body[0]);
const bodyToCompare2 = removeServerGeneratedProperties(body[1]);
expect(bodyToCompare1).to.eql(outputRule1);
expect(bodyToCompare2).to.eql(outputRule2);
});
it('should patch a single rule property of name using an id', async () => {
const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1'));
// patch a simple rule's name
const { body } = await securitySolutionApi
.bulkPatchRules({ body: [{ id: createRuleBody.id, name: 'some other name' }] })
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), await utils.getUsername());
outputRule.name = 'some other name';
outputRule.revision = 1;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).to.eql(outputRule);
});
it('should patch two rule properties of name using the two rules id', async () => {
const createRule1 = await createRule(supertest, log, getSimpleRule('rule-1'));
const createRule2 = await createRule(supertest, log, getSimpleRule('rule-2'));
const username = await utils.getUsername();
// patch both rule names
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [
{ id: createRule1.id, name: 'some other name' },
{ id: createRule2.id, name: 'some other name' },
],
})
.expect(200);
const outputRule1 = updateUsername(getSimpleRuleOutputWithoutRuleId('rule-1'), username);
outputRule1.name = 'some other name';
outputRule1.revision = 1;
const outputRule2 = updateUsername(getSimpleRuleOutputWithoutRuleId('rule-2'), username);
outputRule2.name = 'some other name';
outputRule2.revision = 1;
const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]);
expect(bodyToCompare1).to.eql(outputRule1);
expect(bodyToCompare2).to.eql(outputRule2);
});
it('should bulk disable two rules and migrate their actions', async () => {
const [connector, rule1, rule2] = await Promise.all([
supertest
.post(`/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'My action',
connector_type_id: '.slack',
secrets: {
webhookUrl: 'http://localhost:1234',
},
}),
createRule(supertest, log, getSimpleRule('rule-1')),
createRule(supertest, log, getSimpleRule('rule-2')),
]);
await Promise.all([
createLegacyRuleAction(supertest, rule1.id, connector.body.id),
createLegacyRuleAction(supertest, rule2.id, connector.body.id),
]);
// check for legacy sidecar action
const sidecarActionsResults = await getLegacyActionSO(es);
expect(sidecarActionsResults.hits.hits.length).to.eql(2);
expect(
sidecarActionsResults.hits.hits.map((hit) => hit?._source?.references[0].id).sort()
).to.eql([rule1.id, rule2.id].sort());
// patch a simple rule's name
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [
{ id: rule1.id, enabled: false },
{ id: rule2.id, enabled: false },
],
})
.expect(200);
// legacy sidecar action should be gone
const sidecarActionsPostResults = await getLegacyActionSO(es);
expect(sidecarActionsPostResults.hits.hits.length).to.eql(0);
const username = await utils.getUsername();
// @ts-expect-error
body.forEach((response) => {
const bodyToCompare = removeServerGeneratedProperties(response);
const outputRule = updateUsername(getSimpleRuleOutput(response.rule_id, false), username);
outputRule.actions = [
{
action_type_id: '.slack',
group: 'default',
id: connector.body.id,
params: {
message:
'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
uuid: bodyToCompare.actions[0].uuid,
frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' },
},
];
outputRule.revision = 1;
expect(bodyToCompare).to.eql(outputRule);
});
});
it('should patch a single rule property of name using the auto-generated id', async () => {
const createdBody = await createRule(supertest, log, getSimpleRule('rule-1'));
// patch a simple rule's name
const { body } = await securitySolutionApi
.bulkPatchRules({ body: [{ id: createdBody.id, name: 'some other name' }] })
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), await utils.getUsername());
outputRule.name = 'some other name';
outputRule.revision = 1;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).to.eql(outputRule);
});
it('should not change the revision of a rule when it patches only enabled', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// patch a simple rule's enabled to false
const { body } = await securitySolutionApi
.bulkPatchRules({ body: [{ rule_id: 'rule-1', enabled: false }] })
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), await utils.getUsername());
outputRule.enabled = false;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).to.eql(outputRule);
});
it('should change the revision of a rule when it patches enabled and another property', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// patch a simple rule's enabled to false and another property
const { body } = await securitySolutionApi
.bulkPatchRules({ body: [{ rule_id: 'rule-1', severity: 'low', enabled: false }] })
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), await utils.getUsername());
outputRule.enabled = false;
outputRule.severity = 'low';
outputRule.revision = 1;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).to.eql(outputRule);
});
it('should not change other properties when it does patches', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// patch a simple rule's timeline_title
await securitySolutionApi
.bulkPatchRules({
body: [{ rule_id: 'rule-1', timeline_title: 'some title', timeline_id: 'some id' }],
})
.expect(200);
// patch a simple rule's name
const { body } = await securitySolutionApi
.bulkPatchRules({ body: [{ rule_id: 'rule-1', name: 'some other name' }] })
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), await utils.getUsername());
outputRule.name = 'some other name';
outputRule.timeline_title = 'some title';
outputRule.timeline_id = 'some id';
outputRule.revision = 2;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).to.eql(outputRule);
});
it('should return a 200 but give a 404 in the message if it is given a fake id', async () => {
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [{ id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', name: 'some other name' }],
})
.expect(200);
expect(body).to.eql([
{
id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d',
error: {
status_code: 404,
message: 'id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" not found',
},
},
]);
});
it('should return a 200 but give a 404 in the message if it is given a fake rule_id', async () => {
const { body } = await securitySolutionApi
.bulkPatchRules({ body: [{ rule_id: 'fake_id', name: 'some other name' }] })
.expect(200);
expect(body).to.eql([
{
rule_id: 'fake_id',
error: { status_code: 404, message: 'rule_id: "fake_id" not found' },
},
]);
});
it('should patch one rule property and give an error about a second fake rule_id', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// patch one rule name and give a fake id for the second
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [
{ rule_id: 'rule-1', name: 'some other name' },
{ rule_id: 'fake_id', name: 'some other name' },
],
})
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), await utils.getUsername());
outputRule.name = 'some other name';
outputRule.revision = 1;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect([bodyToCompare, body[1]]).to.eql([
outputRule,
{
error: {
message: 'rule_id: "fake_id" not found',
status_code: 404,
},
rule_id: 'fake_id',
},
]);
});
it('should patch one rule property and give an error about a second fake id', async () => {
const createdBody = await createRule(supertest, log, getSimpleRule('rule-1'));
// patch one rule name and give a fake id for the second
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [
{ id: createdBody.id, name: 'some other name' },
{ id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', name: 'some other name' },
],
})
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), await utils.getUsername());
outputRule.name = 'some other name';
outputRule.revision = 1;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect([bodyToCompare, body[1]]).to.eql([
outputRule,
{
error: {
message: 'id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" not found',
status_code: 404,
},
id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d',
},
]);
});
it('should return a 200 ok but have a 409 conflict if we attempt to patch the rule, which use existing attached rule defult list', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
const ruleWithException = await createRule(supertest, log, {
...getSimpleRule('rule-2'),
exceptions_list: [
{
id: '2',
list_id: '123',
namespace_type: 'single',
type: ExceptionListTypeEnum.RULE_DEFAULT,
},
],
});
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [
{
rule_id: 'rule-1',
exceptions_list: [
{
id: '2',
list_id: '123',
namespace_type: 'single',
type: ExceptionListTypeEnum.RULE_DEFAULT,
},
],
},
],
})
.expect(200);
expect(body).to.eql([
{
error: {
message: `default exception list for rule: rule-1 already exists in rule(s): ${ruleWithException.id}`,
status_code: 409,
},
rule_id: 'rule-1',
},
]);
});
it('should return a 409 if several rules has the same exception rule default list', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
await createRule(supertest, log, getSimpleRule('rule-2'));
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [
{
rule_id: 'rule-1',
exceptions_list: [
{
id: '2',
list_id: '123',
namespace_type: 'single',
type: ExceptionListTypeEnum.RULE_DEFAULT,
},
],
},
{
rule_id: 'rule-2',
exceptions_list: [
{
id: '2',
list_id: '123',
namespace_type: 'single',
type: ExceptionListTypeEnum.RULE_DEFAULT,
},
],
},
],
})
.expect(200);
expect(body).to.eql([
{
error: {
message: 'default exceptions list 2 for rule rule-1 is duplicated',
status_code: 409,
},
rule_id: 'rule-1',
},
{
error: {
message: 'default exceptions list 2 for rule rule-2 is duplicated',
status_code: 409,
},
rule_id: 'rule-2',
},
]);
});
});
describe('legacy investigation fields', () => {
let ruleWithLegacyInvestigationField: Rule<BaseRuleParams>;
let ruleWithLegacyInvestigationFieldEmptyArray: Rule<BaseRuleParams>;
beforeEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
await createAlertsIndex(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 securitySolutionApi
.bulkPatchRules({
body: [
{
rule_id: ruleWithLegacyInvestigationField.params.ruleId,
name: 'some other name',
// @ts-expect-error we are testing the invalid payload
investigation_fields: ['foobar'],
},
],
})
.expect(400);
expect(body.message).to.eql(
'[request body]: 0.investigation_fields: Expected object, received array, 0.investigation_fields: Expected object, received array, 0.investigation_fields: Expected object, received array, 0.investigation_fields: Expected object, received array, 0.investigation_fields: Expected object, received array, and 3 more'
);
});
it('should patch a rule with a legacy investigation field and migrate field', async () => {
// patch a simple rule's name
const { body } = await securitySolutionApi
.bulkPatchRules({
body: [
{ 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');
const isInvestigationFieldMigratedInSo = await checkInvestigationFieldSoValue(
undefined,
{ field_names: ['client.address', 'agent.name'] },
es,
body[0].id
);
expect(isInvestigationFieldMigratedInSo).to.eql(true);
});
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 securitySolutionApi
.bulkPatchRules({
body: [
{
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 isInvestigationFieldMigratedInSo = await checkInvestigationFieldSoValue(
undefined,
{ field_names: [] },
es,
body[0].id
);
expect(isInvestigationFieldMigratedInSo).to.eql(false);
});
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 securitySolutionApi
.bulkPatchRules({
body: [
{
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

@ -1,404 +0,0 @@
/*
* 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 expect from 'expect';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
import {
getSimpleRuleOutput,
getCustomQueryRuleParams,
removeServerGeneratedProperties,
getSimpleRuleOutputWithoutRuleId,
removeServerGeneratedPropertiesIncludingRuleId,
getSimpleRuleUpdate,
getSimpleRule,
updateUsername,
createHistoricalPrebuiltRuleAssetSavedObjects,
installPrebuiltRules,
createRuleAssetSavedObject,
} from '../../../utils';
import {
createAlertsIndex,
deleteAllRules,
createRule,
deleteAllAlerts,
} from '../../../../../../common/utils/security_solution';
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const securitySolutionApi = getService('securitySolutionApi');
const log = getService('log');
const es = getService('es');
const utils = getService('securitySolutionUtils');
// TODO: https://github.com/elastic/kibana/issues/193184 Delete this file and clean up the code
describe.skip('@ess @serverless @skipInServerlessMKI update_rules_bulk', () => {
describe('update rules bulk', () => {
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
afterEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
});
it('should update a single rule property of name using a rule_id', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
const updatedRule = getSimpleRuleUpdate('rule-1');
updatedRule.name = 'some other name';
// update a simple rule's name
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule] })
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.name = 'some other name';
outputRule.revision = 1;
const expectedRule = updateUsername(outputRule, await utils.getUsername());
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).toEqual(expectedRule);
});
it('should update a rule with defaultable fields', async () => {
const ruleUpdateProperties = getCustomQueryRuleParams({
rule_id: 'rule-1',
max_signals: 200,
setup: '# some setup markdown',
related_integrations: [
{ package: 'package-a', version: '^1.2.3' },
{ package: 'package-b', integration: 'integration-b', version: '~1.1.1' },
],
required_fields: [{ name: '@timestamp', type: 'date' }],
});
const expectedRule = {
...ruleUpdateProperties,
required_fields: [{ name: '@timestamp', type: 'date', ecs: true }],
};
await securitySolutionApi.createRule({
body: getCustomQueryRuleParams({ rule_id: 'rule-1' }),
});
const { body: updatedRulesBulkResponse } = await securitySolutionApi
.bulkUpdateRules({
body: [ruleUpdateProperties],
})
.expect(200);
expect(updatedRulesBulkResponse[0]).toMatchObject(expectedRule);
const { body: updatedRule } = await securitySolutionApi
.readRule({
query: { rule_id: 'rule-1' },
})
.expect(200);
expect(updatedRule).toMatchObject(expectedRule);
});
it('should update two rule properties of name using the two rules rule_id', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// create a second simple rule
await securitySolutionApi.createRule({ body: getSimpleRule('rule-2') }).expect(200);
const updatedRule1 = getSimpleRuleUpdate('rule-1');
updatedRule1.name = 'some other name';
const updatedRule2 = getSimpleRuleUpdate('rule-2');
updatedRule2.name = 'some other name';
// update both rule names
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule1, updatedRule2] })
.expect(200);
const username = await utils.getUsername();
const outputRule1 = getSimpleRuleOutput();
outputRule1.name = 'some other name';
outputRule1.revision = 1;
const expectedRule = updateUsername(outputRule1, username);
const outputRule2 = getSimpleRuleOutput('rule-2');
outputRule2.name = 'some other name';
outputRule2.revision = 1;
const expectedRule2 = updateUsername(outputRule2, username);
const bodyToCompare1 = removeServerGeneratedProperties(body[0]);
const bodyToCompare2 = removeServerGeneratedProperties(body[1]);
expect(bodyToCompare1).toEqual(expectedRule);
expect(bodyToCompare2).toEqual(expectedRule2);
});
it('should update a single rule property of name using an id', async () => {
const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1'));
// update a simple rule's name
const updatedRule1 = getSimpleRuleUpdate('rule-1');
updatedRule1.id = createRuleBody.id;
updatedRule1.name = 'some other name';
delete updatedRule1.rule_id;
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule1] })
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.name = 'some other name';
outputRule.revision = 1;
const expectedRule = updateUsername(outputRule, await utils.getUsername());
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).toEqual(expectedRule);
});
it('should update two rule properties of name using the two rules id', async () => {
const createRule1 = await createRule(supertest, log, getSimpleRule('rule-1'));
const createRule2 = await createRule(supertest, log, getSimpleRule('rule-2'));
// update both rule names
const updatedRule1 = getSimpleRuleUpdate('rule-1');
updatedRule1.id = createRule1.id;
updatedRule1.name = 'some other name';
delete updatedRule1.rule_id;
const updatedRule2 = getSimpleRuleUpdate('rule-1');
updatedRule2.id = createRule2.id;
updatedRule2.name = 'some other name';
delete updatedRule2.rule_id;
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule1, updatedRule2] })
.expect(200);
const username = await utils.getUsername();
const outputRule1 = getSimpleRuleOutputWithoutRuleId('rule-1');
outputRule1.name = 'some other name';
outputRule1.revision = 1;
const expectedRule = updateUsername(outputRule1, username);
const outputRule2 = getSimpleRuleOutputWithoutRuleId('rule-2');
outputRule2.name = 'some other name';
outputRule2.revision = 1;
const expectedRule2 = updateUsername(outputRule2, username);
const bodyToCompare1 = removeServerGeneratedPropertiesIncludingRuleId(body[0]);
const bodyToCompare2 = removeServerGeneratedPropertiesIncludingRuleId(body[1]);
expect(bodyToCompare1).toEqual(expectedRule);
expect(bodyToCompare2).toEqual(expectedRule2);
});
it('should update a single rule property of name using the auto-generated id', async () => {
const createdBody = await createRule(supertest, log, getSimpleRule('rule-1'));
// update a simple rule's name
const updatedRule1 = getSimpleRuleUpdate('rule-1');
updatedRule1.id = createdBody.id;
updatedRule1.name = 'some other name';
delete updatedRule1.rule_id;
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule1] })
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.name = 'some other name';
outputRule.revision = 1;
const expectedRule = updateUsername(outputRule, await utils.getUsername());
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).toEqual(expectedRule);
});
it('should change the revision of a rule when it updates enabled and another property', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// update a simple rule's enabled to false and another property
const updatedRule1 = getSimpleRuleUpdate('rule-1');
updatedRule1.severity = 'low';
updatedRule1.enabled = false;
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule1] })
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.enabled = false;
outputRule.severity = 'low';
outputRule.revision = 1;
const expectedRule = updateUsername(outputRule, await utils.getUsername());
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).toEqual(expectedRule);
});
it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// update a simple rule's timeline_title
const ruleUpdate = getSimpleRuleUpdate('rule-1');
ruleUpdate.timeline_title = 'some title';
ruleUpdate.timeline_id = 'some id';
await securitySolutionApi.bulkUpdateRules({ body: [ruleUpdate] }).expect(200);
// update a simple rule's name
const ruleUpdate2 = getSimpleRuleUpdate('rule-1');
ruleUpdate2.name = 'some other name';
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [ruleUpdate2] })
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.name = 'some other name';
outputRule.revision = 2;
const expectedRule = updateUsername(outputRule, await utils.getUsername());
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).toEqual(expectedRule);
});
it('should return a 200 but give a 404 in the message if it is given a fake id', async () => {
const ruleUpdate = getSimpleRuleUpdate('rule-1');
ruleUpdate.id = '1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5';
delete ruleUpdate.rule_id;
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [ruleUpdate] })
.expect(200);
expect(body).toEqual([
{
id: '1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5',
error: {
status_code: 404,
message: 'id: "1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5" not found',
},
},
]);
});
it('should return a 200 but give a 404 in the message if it is given a fake rule_id', async () => {
const ruleUpdate = getSimpleRuleUpdate('rule-1');
ruleUpdate.rule_id = 'fake_id';
delete ruleUpdate.id;
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [ruleUpdate] })
.expect(200);
expect(body).toEqual([
{
rule_id: 'fake_id',
error: { status_code: 404, message: 'rule_id: "fake_id" not found' },
},
]);
});
it('should update one rule property and give an error about a second fake rule_id', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
const ruleUpdate = getSimpleRuleUpdate('rule-1');
ruleUpdate.name = 'some other name';
delete ruleUpdate.id;
const ruleUpdate2 = getSimpleRuleUpdate('fake_id');
ruleUpdate2.name = 'some other name';
delete ruleUpdate.id;
// update one rule name and give a fake id for the second
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [ruleUpdate, ruleUpdate2] })
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.name = 'some other name';
outputRule.revision = 1;
const expectedRule = updateUsername(outputRule, await utils.getUsername());
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect([bodyToCompare, body[1]]).toEqual([
expectedRule,
{
error: {
message: 'rule_id: "fake_id" not found',
status_code: 404,
},
rule_id: 'fake_id',
},
]);
});
it('should update one rule property and give an error about a second fake id', async () => {
const createdBody = await createRule(supertest, log, getSimpleRule('rule-1'));
// update one rule name and give a fake id for the second
const rule1 = getSimpleRuleUpdate();
delete rule1.rule_id;
rule1.id = createdBody.id;
rule1.name = 'some other name';
const rule2 = getSimpleRuleUpdate();
delete rule2.rule_id;
rule2.id = 'b3aa019a-656c-4311-b13b-4d9852e24347';
rule2.name = 'some other name';
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [rule1, rule2] })
.expect(200);
const outputRule = getSimpleRuleOutput();
outputRule.name = 'some other name';
outputRule.revision = 1;
const expectedRule = updateUsername(outputRule, await utils.getUsername());
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect([bodyToCompare, body[1]]).toEqual([
expectedRule,
{
error: {
message: 'id: "b3aa019a-656c-4311-b13b-4d9852e24347" not found',
status_code: 404,
},
id: 'b3aa019a-656c-4311-b13b-4d9852e24347',
},
]);
});
// Unskip: https://github.com/elastic/kibana/issues/195921
it('@skipInServerlessMKI throws an error if rule has external rule source and non-customizable fields are changed', async () => {
// Install base prebuilt detection rule
await createHistoricalPrebuiltRuleAssetSavedObjects(es, [
createRuleAssetSavedObject({ rule_id: 'rule-1', author: ['elastic'] }),
]);
await installPrebuiltRules(es, supertest);
const { body } = await securitySolutionApi
.bulkUpdateRules({
body: [getCustomQueryRuleParams({ rule_id: 'rule-1', author: ['new user'] })],
})
.expect(200);
expect([body[0]]).toEqual([
{
error: {
message: 'Cannot update "author" field for prebuilt rules',
status_code: 400,
},
rule_id: 'rule-1',
},
]);
});
});
});
};

View file

@ -1,879 +0,0 @@
/*
* 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 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 {
NOTIFICATION_THROTTLE_NO_ACTIONS,
NOTIFICATION_THROTTLE_RULE,
NOTIFICATION_DEFAULT_FREQUENCY,
} from '@kbn/security-solution-plugin/common/constants';
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { RuleActionArray, RuleActionThrottle } from '@kbn/securitysolution-io-ts-alerting-types';
import {
getSimpleRuleOutput,
removeServerGeneratedProperties,
getSimpleRuleUpdate,
getSimpleRule,
createLegacyRuleAction,
getLegacyActionSO,
removeServerGeneratedPropertiesIncludingRuleId,
getSimpleRuleWithoutRuleId,
getSimpleRuleOutputWithoutRuleId,
getRuleSavedObjectWithLegacyInvestigationFields,
createRuleThroughAlertingEndpoint,
getRuleSavedObjectWithLegacyInvestigationFieldsEmptyArray,
getRuleSOById,
removeUUIDFromActions,
getActionsWithFrequencies,
getActionsWithoutFrequencies,
getSomeActionsWithFrequencies,
updateUsername,
} from '../../../utils';
import {
createAlertsIndex,
deleteAllRules,
deleteAllAlerts,
createRule,
} from '../../../../../../common/utils/security_solution';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const securitySolutionApi = getService('securitySolutionApi');
const log = getService('log');
const es = getService('es');
const utils = getService('securitySolutionUtils');
let username: string;
// TODO: https://github.com/elastic/kibana/issues/193184 Delete this file and clean up the code
describe.skip('@ess update_rules_bulk', () => {
before(async () => {
username = await utils.getUsername();
});
describe('deprecations', () => {
afterEach(async () => {
await deleteAllRules(supertest, log);
});
it('should return a warning header', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
const updatedRule = getSimpleRuleUpdate('rule-1');
const { header } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule] })
.expect(200);
expect(header.warning).to.be(
'299 Kibana "Deprecated endpoint: /api/detection_engine/rules/_bulk_update API is deprecated since v8.2. Please use the /api/detection_engine/rules/_bulk_action API instead. See https://www.elastic.co/guide/en/security/master/rule-api-overview.html for more detail."'
);
});
});
describe('update rules bulk', () => {
beforeEach(async () => {
await createAlertsIndex(supertest, log);
});
afterEach(async () => {
await deleteAllAlerts(supertest, log, es);
await deleteAllRules(supertest, log);
});
it('should update a single rule property of name using a rule_id', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
const updatedRule = getSimpleRuleUpdate('rule-1');
updatedRule.name = 'some other name';
// update a simple rule's name
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule] })
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), username);
outputRule.name = 'some other name';
outputRule.revision = 1;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).to.eql(outputRule);
});
it('should update two rule properties of name using the two rules rule_id', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// create a second simple rule
await securitySolutionApi.createRule({ body: getSimpleRuleUpdate('rule-2') }).expect(200);
const updatedRule1 = getSimpleRuleUpdate('rule-1');
updatedRule1.name = 'some other name';
const updatedRule2 = getSimpleRuleUpdate('rule-2');
updatedRule2.name = 'some other name';
// update both rule names
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule1, updatedRule2] })
.expect(200);
const outputRule1 = updateUsername(getSimpleRuleOutput(), username);
outputRule1.name = 'some other name';
outputRule1.revision = 1;
const outputRule2 = updateUsername(getSimpleRuleOutput('rule-2'), username);
outputRule2.name = 'some other name';
outputRule2.revision = 1;
const bodyToCompare1 = removeServerGeneratedProperties(body[0]);
const bodyToCompare2 = removeServerGeneratedProperties(body[1]);
expect(bodyToCompare1).to.eql(outputRule1);
expect(bodyToCompare2).to.eql(outputRule2);
});
it('should update two rule properties of name using the two rules rule_id and migrate actions', async () => {
const connector = await supertest
.post(`/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'My action',
connector_type_id: '.slack',
secrets: {
webhookUrl: 'http://localhost:1234',
},
});
const action1 = {
group: 'default',
id: connector.body.id,
action_type_id: connector.body.connector_type_id,
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
};
const [rule1, rule2] = await Promise.all([
createRule(supertest, log, { ...getSimpleRule('rule-1'), actions: [action1] }),
createRule(supertest, log, { ...getSimpleRule('rule-2'), actions: [action1] }),
]);
await Promise.all([
createLegacyRuleAction(supertest, rule1.id, connector.body.id),
createLegacyRuleAction(supertest, rule2.id, connector.body.id),
]);
// check for legacy sidecar action
const sidecarActionsResults = await getLegacyActionSO(es);
expect(sidecarActionsResults.hits.hits.length).to.eql(2);
expect(
sidecarActionsResults.hits.hits.map((hit) => hit?._source?.references[0].id).sort()
).to.eql([rule1.id, rule2.id].sort());
const updatedRule1 = getSimpleRuleUpdate('rule-1');
updatedRule1.name = 'some other name';
updatedRule1.actions = [action1];
const updatedRule2 = getSimpleRuleUpdate('rule-2');
updatedRule2.name = 'some other name';
updatedRule2.actions = [action1];
// update both rule names
const { body }: { body: RuleResponse[] } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule1, updatedRule2] })
.expect(200);
// legacy sidecar action should be gone
const sidecarActionsPostResults = await getLegacyActionSO(es);
expect(sidecarActionsPostResults.hits.hits.length).to.eql(0);
body.forEach((response) => {
const bodyToCompare = removeServerGeneratedProperties(response);
const outputRule = updateUsername(getSimpleRuleOutput(response.rule_id), username);
outputRule.name = 'some other name';
outputRule.revision = 1;
outputRule.actions = [
{
action_type_id: '.slack',
group: 'default',
id: connector.body.id,
params: {
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
},
uuid: bodyToCompare.actions[0].uuid,
frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' },
},
];
expect(bodyToCompare).to.eql(outputRule);
});
});
it('should update two rule properties of name using the two rules rule_id and remove actions', async () => {
const connector = await supertest
.post(`/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'My action',
connector_type_id: '.slack',
secrets: {
webhookUrl: 'http://localhost:1234',
},
});
const action1 = {
group: 'default',
id: connector.body.id,
action_type_id: connector.body.connector_type_id,
params: {
message: 'message',
},
};
const [rule1, rule2] = await Promise.all([
createRule(supertest, log, { ...getSimpleRule('rule-1'), actions: [action1] }),
createRule(supertest, log, { ...getSimpleRule('rule-2'), actions: [action1] }),
]);
await Promise.all([
createLegacyRuleAction(supertest, rule1.id, connector.body.id),
createLegacyRuleAction(supertest, rule2.id, connector.body.id),
]);
const updatedRule1 = getSimpleRuleUpdate('rule-1');
updatedRule1.name = 'some other name';
const updatedRule2 = getSimpleRuleUpdate('rule-2');
updatedRule2.name = 'some other name';
// update both rule names
const { body }: { body: RuleResponse[] } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule1, updatedRule2] })
.expect(200);
body.forEach((response) => {
const outputRule = updateUsername(getSimpleRuleOutput(response.rule_id), username);
outputRule.name = 'some other name';
outputRule.revision = 1;
outputRule.actions = [];
const bodyToCompare = removeServerGeneratedProperties(response);
expect(bodyToCompare).to.eql(outputRule);
});
});
it('should update a single rule property of name using an id', async () => {
const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1'));
// update a simple rule's name
const updatedRule1 = getSimpleRuleUpdate('rule-1');
updatedRule1.id = createRuleBody.id;
updatedRule1.name = 'some other name';
delete updatedRule1.rule_id;
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule1] })
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), username);
outputRule.name = 'some other name';
outputRule.revision = 1;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).to.eql(outputRule);
});
it('should update two rule properties of name using the two rules id', async () => {
const createRule1 = await createRule(supertest, log, getSimpleRule('rule-1'));
const createRule2 = await createRule(supertest, log, getSimpleRule('rule-2'));
// update both rule names
const updatedRule1 = getSimpleRuleUpdate('rule-1');
updatedRule1.id = createRule1.id;
updatedRule1.name = 'some other name';
delete updatedRule1.rule_id;
const updatedRule2 = getSimpleRuleUpdate('rule-1');
updatedRule2.id = createRule2.id;
updatedRule2.name = 'some other name';
delete updatedRule2.rule_id;
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule1, updatedRule2] })
.expect(200);
const outputRule1 = updateUsername(getSimpleRuleOutput('rule-1'), username);
outputRule1.name = 'some other name';
outputRule1.revision = 1;
const outputRule2 = updateUsername(getSimpleRuleOutput('rule-2'), username);
outputRule2.name = 'some other name';
outputRule2.revision = 1;
const bodyToCompare1 = removeServerGeneratedProperties(body[0]);
const bodyToCompare2 = removeServerGeneratedProperties(body[1]);
expect(bodyToCompare1).to.eql(outputRule1);
expect(bodyToCompare2).to.eql(outputRule2);
});
it('should update a single rule property of name using the auto-generated id', async () => {
const createdBody = await createRule(supertest, log, getSimpleRule('rule-1'));
// update a simple rule's name
const updatedRule1 = getSimpleRuleUpdate('rule-1');
updatedRule1.id = createdBody.id;
updatedRule1.name = 'some other name';
delete updatedRule1.rule_id;
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule1] })
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), username);
outputRule.name = 'some other name';
outputRule.revision = 1;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).to.eql(outputRule);
});
it('should change the revision of a rule when it updates enabled and another property', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// update a simple rule's enabled to false and another property
const updatedRule1 = getSimpleRuleUpdate('rule-1');
updatedRule1.severity = 'low';
updatedRule1.enabled = false;
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [updatedRule1] })
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), username);
outputRule.enabled = false;
outputRule.severity = 'low';
outputRule.revision = 1;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).to.eql(outputRule);
});
it('should change other properties when it does updates and effectively delete them such as timeline_title', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
// update a simple rule's timeline_title
const ruleUpdate = getSimpleRuleUpdate('rule-1');
ruleUpdate.timeline_title = 'some title';
ruleUpdate.timeline_id = 'some id';
await securitySolutionApi.bulkUpdateRules({ body: [ruleUpdate] }).expect(200);
// update a simple rule's name
const ruleUpdate2 = getSimpleRuleUpdate('rule-1');
ruleUpdate2.name = 'some other name';
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [ruleUpdate2] })
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), username);
outputRule.name = 'some other name';
outputRule.revision = 2;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect(bodyToCompare).to.eql(outputRule);
});
it('should return a 200 but give a 404 in the message if it is given a fake id', async () => {
const ruleUpdate = getSimpleRuleUpdate('rule-1');
ruleUpdate.id = '1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5';
delete ruleUpdate.rule_id;
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [ruleUpdate] })
.expect(200);
expect(body).to.eql([
{
id: '1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5',
error: {
status_code: 404,
message: 'id: "1fd52120-d3a9-4e7a-b23c-96c0e1a74ae5" not found',
},
},
]);
});
it('should return a 200 but give a 404 in the message if it is given a fake rule_id', async () => {
const ruleUpdate = getSimpleRuleUpdate('rule-1');
ruleUpdate.rule_id = 'fake_id';
delete ruleUpdate.id;
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [ruleUpdate] })
.expect(200);
expect(body).to.eql([
{
rule_id: 'fake_id',
error: { status_code: 404, message: 'rule_id: "fake_id" not found' },
},
]);
});
it('should update one rule property and give an error about a second fake rule_id', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
const ruleUpdate = getSimpleRuleUpdate('rule-1');
ruleUpdate.name = 'some other name';
delete ruleUpdate.id;
const ruleUpdate2 = getSimpleRuleUpdate('fake_id');
ruleUpdate2.name = 'some other name';
delete ruleUpdate.id;
// update one rule name and give a fake id for the second
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [ruleUpdate, ruleUpdate2] })
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), username);
outputRule.name = 'some other name';
outputRule.revision = 1;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect([bodyToCompare, body[1]]).to.eql([
outputRule,
{
error: {
message: 'rule_id: "fake_id" not found',
status_code: 404,
},
rule_id: 'fake_id',
},
]);
});
it('should update one rule property and give an error about a second fake id', async () => {
const createdBody = await createRule(supertest, log, getSimpleRule('rule-1'));
// update one rule name and give a fake id for the second
const rule1 = getSimpleRuleUpdate();
delete rule1.rule_id;
rule1.id = createdBody.id;
rule1.name = 'some other name';
const rule2 = getSimpleRuleUpdate();
delete rule2.rule_id;
rule2.id = 'b3aa019a-656c-4311-b13b-4d9852e24347';
rule2.name = 'some other name';
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [rule1, rule2] })
.expect(200);
const outputRule = updateUsername(getSimpleRuleOutput(), username);
outputRule.name = 'some other name';
outputRule.revision = 1;
const bodyToCompare = removeServerGeneratedProperties(body[0]);
expect([bodyToCompare, body[1]]).to.eql([
outputRule,
{
error: {
message: 'id: "b3aa019a-656c-4311-b13b-4d9852e24347" not found',
status_code: 404,
},
id: 'b3aa019a-656c-4311-b13b-4d9852e24347',
},
]);
});
it('should return a 200 ok but have a 409 conflict if we attempt to update the rule, which use existing attached rule defult list', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
const ruleWithException = await createRule(supertest, log, {
...getSimpleRule('rule-2'),
exceptions_list: [
{
id: '2',
list_id: '123',
namespace_type: 'single',
type: ExceptionListTypeEnum.RULE_DEFAULT,
},
],
});
const rule1 = getSimpleRuleUpdate('rule-1');
rule1.name = 'some other name';
const { body } = await securitySolutionApi
.bulkUpdateRules({
body: [
{
...rule1,
exceptions_list: [
{
id: '2',
list_id: '123',
namespace_type: 'single',
type: ExceptionListTypeEnum.RULE_DEFAULT,
},
],
},
],
})
.expect(200);
expect(body).to.eql([
{
error: {
message: `default exception list for rule: rule-1 already exists in rule(s): ${ruleWithException.id}`,
status_code: 409,
},
rule_id: 'rule-1',
},
]);
});
it('should return a 409 if several rules has the same exception rule default list', async () => {
await createRule(supertest, log, getSimpleRule('rule-1'));
await createRule(supertest, log, getSimpleRule('rule-2'));
const rule1 = getSimpleRuleUpdate('rule-1');
rule1.name = 'some other name';
const rule2 = getSimpleRuleUpdate('rule-2');
rule2.name = 'some other name';
const { body } = await securitySolutionApi
.bulkUpdateRules({
body: [
{
...rule1,
exceptions_list: [
{
id: '2',
list_id: '123',
namespace_type: 'single',
type: ExceptionListTypeEnum.RULE_DEFAULT,
},
],
},
{
...rule2,
exceptions_list: [
{
id: '2',
list_id: '123',
namespace_type: 'single',
type: ExceptionListTypeEnum.RULE_DEFAULT,
},
],
},
],
})
.expect(200);
expect(body).to.eql([
{
error: {
message: 'default exceptions list 2 for rule rule-1 is duplicated',
status_code: 409,
},
rule_id: 'rule-1',
},
{
error: {
message: 'default exceptions list 2 for rule rule-2 is duplicated',
status_code: 409,
},
rule_id: 'rule-2',
},
]);
});
});
describe('bulk per-action frequencies', () => {
const bulkUpdateSingleRule = async (
ruleId: string,
throttle: RuleActionThrottle | undefined,
actions: RuleActionArray
) => {
// update a simple rule's `throttle` and `actions`
const ruleToUpdate = getSimpleRuleUpdate();
ruleToUpdate.throttle = throttle;
ruleToUpdate.actions = actions;
ruleToUpdate.id = ruleId;
delete ruleToUpdate.rule_id;
const { body } = await securitySolutionApi
.bulkUpdateRules({ body: [ruleToUpdate] })
.expect(200);
const updatedRule = body[0];
updatedRule.actions = removeUUIDFromActions(updatedRule.actions);
return removeServerGeneratedPropertiesIncludingRuleId(updatedRule);
};
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());
// update a simple rule's `throttle` and `actions`
const updatedRule = await bulkUpdateSingleRule(
createdRule.id,
throttle,
actionsWithoutFrequencies
);
const expectedRule = updateUsername(getSimpleRuleOutputWithoutRuleId(), username);
expectedRule.revision = 1;
expectedRule.actions = actionsWithoutFrequencies.map((action) => ({
...action,
frequency: NOTIFICATION_DEFAULT_FREQUENCY,
}));
expect(updatedRule).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());
// update a simple rule's `throttle` and `actions`
// update a simple rule's `throttle` and `actions`
const updatedRule = await bulkUpdateSingleRule(
createdRule.id,
throttle,
actionsWithoutFrequencies
);
const expectedRule = updateUsername(getSimpleRuleOutputWithoutRuleId(), username);
expectedRule.revision = 1;
expectedRule.actions = actionsWithoutFrequencies.map((action) => ({
...action,
frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' },
}));
expect(updatedRule).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());
// update a simple rule's `throttle` and `actions`
const updatedRule = await bulkUpdateSingleRule(
createdRule.id,
throttle,
actionsWithFrequencies
);
const expectedRule = updateUsername(getSimpleRuleOutputWithoutRuleId(), username);
expectedRule.revision = 1;
expectedRule.actions = actionsWithFrequencies;
expect(updatedRule).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());
// update a simple rule's `throttle` and `actions`
const updatedRule = await bulkUpdateSingleRule(
createdRule.id,
throttle,
someActionsWithFrequencies
);
const expectedRule = updateUsername(getSimpleRuleOutputWithoutRuleId(), username);
expectedRule.revision = 1;
expectedRule.actions = someActionsWithFrequencies.map((action) => ({
...action,
frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY,
}));
expect(updatedRule).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());
// update a simple rule's `throttle` and `actions`
const updatedRule = await bulkUpdateSingleRule(
createdRule.id,
throttle,
someActionsWithFrequencies
);
const expectedRule = updateUsername(getSimpleRuleOutputWithoutRuleId(), username);
expectedRule.revision = 1;
expectedRule.actions = someActionsWithFrequencies.map((action) => ({
...action,
frequency: action.frequency ?? {
summary: true,
throttle,
notifyWhen: 'onThrottleInterval',
},
}));
expect(updatedRule).to.eql(expectedRule);
});
});
});
});
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 createAlertsIndex(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 securitySolutionApi
.bulkUpdateRules({
body: [
{
...getSimpleRule(),
name: 'New name',
rule_id: ruleWithLegacyInvestigationField.params.ruleId,
// @ts-expect-error testing invalid payload here
investigation_fields: ['client.foo'],
},
],
})
.expect(400);
expect(body.message).to.eql(
'[request body]: 0.investigation_fields: Expected object, received array'
);
});
it('updates a rule with legacy investigation fields and transforms field in response', async () => {
// update rule
const { body } = await securitySolutionApi
.bulkUpdateRules({
body: [
{
...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'],
});
});
});
});
};