mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
# 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:
parent
9ebac082ac
commit
0c817d1b7d
45 changed files with 149 additions and 6720 deletions
|
@ -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:
|
||||
|
|
|
@ -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;
|
|
@ -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'
|
|
@ -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"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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'
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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'
|
|
@ -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"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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'
|
|
@ -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"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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]));
|
|
@ -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'
|
|
@ -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'"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)}"`,
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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 [];
|
||||
};
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
);
|
||||
};
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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`);
|
||||
});
|
||||
});
|
|
@ -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
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue