diff --git a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap index c84a3565d48f6..4dc2abbc5f6a8 100644 --- a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap +++ b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap @@ -6135,6 +6135,175 @@ Object { "query": Object { "type": "string", }, + "responseActions": Object { + "items": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".osquery", + "type": "string", + }, + "params": Object { + "additionalProperties": false, + "properties": Object { + "ecsMapping": Object { + "additionalProperties": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "value": Object { + "anyOf": Array [ + Object { + "type": "string", + }, + Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + ], + }, + }, + "type": "object", + }, + "properties": Object {}, + "type": "object", + }, + "packId": Object { + "type": "string", + }, + "queries": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "ecs_mapping": Object { + "$ref": "#/allOf/1/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping", + }, + "id": Object { + "type": "string", + }, + "platform": Object { + "type": "string", + }, + "query": Object { + "type": "string", + }, + "removed": Object { + "type": "boolean", + }, + "snapshot": Object { + "type": "boolean", + }, + "version": Object { + "type": "string", + }, + }, + "required": Array [ + "id", + "query", + ], + "type": "object", + }, + "type": "array", + }, + "query": Object { + "type": "string", + }, + "savedQueryId": Object { + "type": "string", + }, + "timeout": Object { + "type": "number", + }, + }, + "type": "object", + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".endpoint", + "type": "string", + }, + "params": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "const": "isolate", + "type": "string", + }, + "comment": Object { + "type": "string", + }, + }, + "required": Array [ + "command", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "enum": Array [ + "kill-process", + "suspend-process", + ], + "type": "string", + }, + "comment": Object { + "type": "string", + }, + "config": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "overwrite": Object { + "default": true, + "type": "boolean", + }, + }, + "required": Array [ + "field", + ], + "type": "object", + }, + }, + "required": Array [ + "command", + "config", + ], + "type": "object", + }, + ], + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", + }, + ], + }, + "type": "array", + }, "tiebreakerField": Object { "type": "string", }, @@ -7687,6 +7856,175 @@ Object { "query": Object { "type": "string", }, + "responseActions": Object { + "items": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".osquery", + "type": "string", + }, + "params": Object { + "additionalProperties": false, + "properties": Object { + "ecsMapping": Object { + "additionalProperties": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "value": Object { + "anyOf": Array [ + Object { + "type": "string", + }, + Object { + "items": Object { + "type": "string", + }, + "type": "array", + }, + ], + }, + }, + "type": "object", + }, + "properties": Object {}, + "type": "object", + }, + "packId": Object { + "type": "string", + }, + "queries": Object { + "items": Object { + "additionalProperties": false, + "properties": Object { + "ecs_mapping": Object { + "$ref": "#/allOf/1/properties/responseActions/items/anyOf/0/properties/params/properties/ecsMapping", + }, + "id": Object { + "type": "string", + }, + "platform": Object { + "type": "string", + }, + "query": Object { + "type": "string", + }, + "removed": Object { + "type": "boolean", + }, + "snapshot": Object { + "type": "boolean", + }, + "version": Object { + "type": "string", + }, + }, + "required": Array [ + "id", + "query", + ], + "type": "object", + }, + "type": "array", + }, + "query": Object { + "type": "string", + }, + "savedQueryId": Object { + "type": "string", + }, + "timeout": Object { + "type": "number", + }, + }, + "type": "object", + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "actionTypeId": Object { + "const": ".endpoint", + "type": "string", + }, + "params": Object { + "anyOf": Array [ + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "const": "isolate", + "type": "string", + }, + "comment": Object { + "type": "string", + }, + }, + "required": Array [ + "command", + ], + "type": "object", + }, + Object { + "additionalProperties": false, + "properties": Object { + "command": Object { + "enum": Array [ + "kill-process", + "suspend-process", + ], + "type": "string", + }, + "comment": Object { + "type": "string", + }, + "config": Object { + "additionalProperties": false, + "properties": Object { + "field": Object { + "type": "string", + }, + "overwrite": Object { + "default": true, + "type": "boolean", + }, + }, + "required": Array [ + "field", + ], + "type": "object", + }, + }, + "required": Array [ + "command", + "config", + ], + "type": "object", + }, + ], + }, + }, + "required": Array [ + "actionTypeId", + "params", + ], + "type": "object", + }, + ], + }, + "type": "array", + }, "type": Object { "const": "new_terms", "type": "string", diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts index 2d3dbcd3f436f..a723eb8e7da89 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts @@ -224,6 +224,7 @@ export const EqlOptionalFields = z.object({ tiebreaker_field: TiebreakerField.optional(), timestamp_field: TimestampField.optional(), alert_suppression: AlertSuppression.optional(), + response_actions: z.array(ResponseAction).optional(), }); export type EqlRuleCreateFields = z.infer; @@ -521,6 +522,7 @@ export const NewTermsRuleOptionalFields = z.object({ data_view_id: DataViewId.optional(), filters: RuleFilterArray.optional(), alert_suppression: AlertSuppression.optional(), + response_actions: z.array(ResponseAction).optional(), }); export type NewTermsRuleDefaultableFields = z.infer; @@ -574,6 +576,7 @@ export const EsqlRuleRequiredFields = z.object({ export type EsqlRuleOptionalFields = z.infer; export const EsqlRuleOptionalFields = z.object({ alert_suppression: AlertSuppression.optional(), + response_actions: z.array(ResponseAction).optional(), }); export type EsqlRulePatchFields = z.infer; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml index 4ade72c15fbb9..ca2f325c8f713 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml @@ -292,6 +292,10 @@ components: $ref: './specific_attributes/eql_attributes.schema.yaml#/components/schemas/TimestampField' alert_suppression: $ref: './common_attributes.schema.yaml#/components/schemas/AlertSuppression' + response_actions: + type: array + items: + $ref: '../rule_response_actions/response_actions.schema.yaml#/components/schemas/ResponseAction' EqlRuleCreateFields: allOf: @@ -762,6 +766,10 @@ components: $ref: './common_attributes.schema.yaml#/components/schemas/RuleFilterArray' alert_suppression: $ref: './common_attributes.schema.yaml#/components/schemas/AlertSuppression' + response_actions: + type: array + items: + $ref: '../rule_response_actions/response_actions.schema.yaml#/components/schemas/ResponseAction' NewTermsRuleDefaultableFields: type: object @@ -840,6 +848,10 @@ components: properties: alert_suppression: $ref: './common_attributes.schema.yaml#/components/schemas/AlertSuppression' + response_actions: + type: array + items: + $ref: '../rule_response_actions/response_actions.schema.yaml#/components/schemas/ResponseAction' EsqlRulePatchFields: allOf: diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index e0cefdebecd93..503e0c58ff46e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -93,3 +93,14 @@ export const isSuppressionRuleConfiguredWithMissingFields = (ruleType: Type) => export const isSuppressionRuleInGA = (ruleType: Type): boolean => { return isSuppressibleAlertRule(ruleType) && SUPPRESSIBLE_ALERT_RULES_GA.includes(ruleType); }; + +export const shouldShowResponseActions = ( + ruleType: Type | undefined, + automatedResponseActionsForMoreRulesEnabled: boolean +) => { + return ( + isQueryRule(ruleType) || + (automatedResponseActionsForMoreRulesEnabled && + (isEsqlRule(ruleType) || isEqlRule(ruleType) || isNewTermsRule(ruleType))) + ); +}; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index e11965653526f..121c8d6a97a1a 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -52,6 +52,11 @@ export const allowedExperimentalValues = Object.freeze({ */ automatedProcessActionsEnabled: true, + /** + * Temporary feature flag to enable the Response Actions in Rules UI - intermediate release + */ + automatedResponseActionsForMoreRulesEnabled: false, + /** * Enables the ability to send Response actions to SentinelOne and persist the results * in ES. Adds API changes to support `agentType` and supports `isolate` and `release` diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml index dcee1694a4aeb..8642113778fe0 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -2042,6 +2042,10 @@ components: $ref: '#/components/schemas/RuleFilterArray' index: $ref: '#/components/schemas/IndexPatternArray' + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array tiebreaker_field: $ref: '#/components/schemas/TiebreakerField' timestamp_field: @@ -2729,6 +2733,10 @@ components: properties: alert_suppression: $ref: '#/components/schemas/AlertSuppression' + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array EsqlRulePatchProps: allOf: - type: object @@ -3873,6 +3881,10 @@ components: $ref: '#/components/schemas/RuleFilterArray' index: $ref: '#/components/schemas/IndexPatternArray' + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array NewTermsRulePatchFields: allOf: - type: object diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml index e3a294c9f92a5..514c4c87405cd 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -1316,6 +1316,10 @@ components: $ref: '#/components/schemas/RuleFilterArray' index: $ref: '#/components/schemas/IndexPatternArray' + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array tiebreaker_field: $ref: '#/components/schemas/TiebreakerField' timestamp_field: @@ -2003,6 +2007,10 @@ components: properties: alert_suppression: $ref: '#/components/schemas/AlertSuppression' + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array EsqlRulePatchProps: allOf: - type: object @@ -3026,6 +3034,10 @@ components: $ref: '#/components/schemas/RuleFilterArray' index: $ref: '#/components/schemas/IndexPatternArray' + response_actions: + items: + $ref: '#/components/schemas/ResponseAction' + type: array NewTermsRulePatchFields: allOf: - type: object diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/step_rule_actions/index.tsx index b555054a75e0c..5838c85281123 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/step_rule_actions/index.tsx @@ -16,8 +16,9 @@ import type { } from '@kbn/triggers-actions-ui-plugin/public'; import { UseArray } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { shouldShowResponseActions } from '../../../../../common/detection_engine/utils'; import type { RuleObjectId } from '../../../../../common/api/detection_engine/model/rule_schema'; -import { isQueryRule } from '../../../../../common/detection_engine/utils'; import { ResponseActionsForm } from '../../../rule_response_actions/response_actions_form'; import type { RuleStepProps, @@ -84,6 +85,9 @@ const StepRuleActionsComponent: FC = ({ const { services: { application }, } = useKibana(); + const automatedResponseActionsForMoreRulesEnabled = useIsExperimentalFeatureEnabled( + 'automatedResponseActionsForMoreRulesEnabled' + ); const displayActionsOptions = useMemo( () => ( <> @@ -101,7 +105,7 @@ const StepRuleActionsComponent: FC = ({ [actionMessageParams, summaryActionMessageParams] ); const displayResponseActionsOptions = useMemo(() => { - if (isQueryRule(ruleType)) { + if (shouldShowResponseActions(ruleType, automatedResponseActionsForMoreRulesEnabled)) { return ( {ResponseActionsForm} @@ -109,7 +113,7 @@ const StepRuleActionsComponent: FC = ({ ); } return null; - }, [ruleType]); + }, [ruleType, automatedResponseActionsForMoreRulesEnabled]); // only display the actions dropdown if the user has "read" privileges for actions const displayActionsDropDown = useMemo(() => { return application.capabilities.actions.show ? ( diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/form.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/form.cy.ts index bc909bb62a30e..4b1b8e728e8c2 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/form.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/form.cy.ts @@ -12,6 +12,8 @@ import { tryAddingDisabledResponseAction, validateAvailableCommands, visitRuleActions, + selectIsolateAndSaveWithoutEnabling, + fillUpNewEsqlRule, } from '../../tasks/response_actions'; import { cleanupRule, generateRandomStringName, loadRule } from '../../tasks/api_fixtures'; import { ResponseActionTypesEnum } from '../../../../../common/api/detection_engine'; @@ -28,6 +30,7 @@ describe( kbnServerArgs: [ `--xpack.securitySolution.enableExperimental=${JSON.stringify([ 'automatedProcessActionsEnabled', + 'automatedResponseActionsForMoreRulesEnabled', ])}`, ], }, @@ -202,6 +205,23 @@ describe( }); }); + describe('User should be able to add response action to ESQL rule', () => { + const [ruleName, ruleDescription] = generateRandomStringName(2); + + beforeEach(() => { + login(ROLE.soc_manager); + }); + + it('create and save endpoint response action inside of a rule', () => { + const query = 'FROM * METADATA _index, _id'; + fillUpNewEsqlRule(ruleName, ruleDescription, query); + addEndpointResponseAction(); + focusAndOpenCommandDropdown(); + validateAvailableCommands(); + selectIsolateAndSaveWithoutEnabling(ruleName); + }); + }); + describe('User should not see endpoint action when no rbac', () => { const [ruleName, ruleDescription] = generateRandomStringName(2); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts index 0e46b99c40d72..715f8adc972f5 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts @@ -42,6 +42,12 @@ export const validateAvailableCommands = () => { cy.getByTestSubj(`command-type-${command}`); }); }; +export const selectIsolateAndSaveWithoutEnabling = (ruleName: string) => { + cy.getByTestSubj(`command-type-isolate`).click(); + cy.getByTestSubj('create-enabled-false').click(); + cy.contains(`${ruleName} was created`); +}; + export const addEndpointResponseAction = () => { cy.getByTestSubj('response-actions-wrapper').within(() => { cy.getByTestSubj('Elastic Defend-response-action-type-selection-option').click(); @@ -69,6 +75,26 @@ export const fillUpNewRule = (name = 'Test', description = 'Test') => { cy.getByTestSubj('about-continue').click(); cy.getByTestSubj('schedule-continue').click(); }; +export const fillUpNewEsqlRule = (name = 'Test', description = 'Test', query: string) => { + loadPage('app/security/rules/management'); + cy.getByTestSubj('create-new-rule').click(); + cy.getByTestSubj('stepDefineRule').within(() => { + cy.getByTestSubj('esqlRuleType').click(); + cy.getByTestSubj('detectionEngineStepDefineRuleEsqlQueryBar').within(() => { + cy.getByTestSubj('globalQueryBar').click(); + cy.getByTestSubj('kibanaCodeEditor').type(query); + }); + }); + cy.getByTestSubj('define-continue').click(); + cy.getByTestSubj('detectionEngineStepAboutRuleName').within(() => { + cy.getByTestSubj('input').type(name); + }); + cy.getByTestSubj('detectionEngineStepAboutRuleDescription').within(() => { + cy.getByTestSubj('input').type(description); + }); + cy.getByTestSubj('about-continue').click(); + cy.getByTestSubj('schedule-continue').click(); +}; export const visitRuleActions = (ruleId: string) => { loadPage(`app/security/rules/id/${ruleId}/edit`); cy.getByTestSubj('edit-rule-actions-tab').should('exist'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts index ee028cfc7f203..45a561996e0a9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts @@ -51,6 +51,7 @@ describe('Prebuilt rule asset schema', () => { // See: detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts const omittedBaseFields = [ 'actions', + 'response_actions', 'throttle', 'meta', 'output_index', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts index 6267be09652e8..2d7b056f86248 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts @@ -63,14 +63,14 @@ const TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_SAVED_QUERY_RULES = export type TypeSpecificFields = z.infer; export const TypeSpecificFields = z.discriminatedUnion('type', [ - EqlRuleCreateFields, + EqlRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES), QueryRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES), SavedQueryRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_SAVED_QUERY_RULES), ThresholdRuleCreateFields, ThreatMatchRuleCreateFields, MachineLearningRuleCreateFields, - NewTermsRuleCreateFields, - EsqlRuleCreateFields, + NewTermsRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES), + EsqlRuleCreateFields.omit(TYPE_SPECIFIC_FIELDS_TO_OMIT_FROM_QUERY_RULES), ]); // Make sure the type-specific fields contain all the same rule types as the type-specific rule params. diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/create_rule/route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/create_rule/route.test.ts index 7441aec8c8fa5..b0d0b202341d3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/create_rule/route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/create_rule/route.test.ts @@ -16,7 +16,12 @@ import { } from '../../../../routes/__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../../../../routes/__mocks__'; import { createRuleRoute } from './route'; -import { getCreateRulesSchemaMock } from '../../../../../../../common/api/detection_engine/model/rule_schema/mocks'; +import { + getCreateEqlRuleSchemaMock, + getCreateEsqlRulesSchemaMock, + getCreateNewTermsRulesSchemaMock, + 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 { HttpAuthzError } from '../../../../../machine_learning/validation'; @@ -181,20 +186,29 @@ describe('Create rule route', () => { }, }); const defaultAction = getResponseAction(); + const ruleTypes: Array<[string, () => object]> = [ + ['query', getCreateRulesSchemaMock], + ['esql', getCreateEsqlRulesSchemaMock], + ['eql', getCreateEqlRuleSchemaMock], + ['new_terms', getCreateNewTermsRulesSchemaMock], + ]; - test('is successful', async () => { - const request = requestMock.create({ - method: 'post', - path: DETECTION_ENGINE_RULES_URL, - body: { - ...getCreateRulesSchemaMock(), - response_actions: [defaultAction], - }, - }); + test.each(ruleTypes)( + 'is successful for %s rule', + async (ruleType: string, schemaMock: (ruleId: string) => object) => { + const request = requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_RULES_URL, + body: { + ...schemaMock(`rule-${ruleType}`), + response_actions: [defaultAction], + }, + }); - const response = await server.inject(request, requestContextMock.convertContext(context)); - expect(response.status).toEqual(200); - }); + const response = await server.inject(request, requestContextMock.convertContext(context)); + expect(response.status).toEqual(200); + } + ); test('fails when isolate rbac is set to false', async () => { (context.securitySolution.getEndpointAuthz as jest.Mock).mockReturnValue(() => ({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/update_rule/route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/update_rule/route.test.ts index 87f42a014c1d2..315ab9e80a5de 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/update_rule/route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/update_rule/route.test.ts @@ -17,6 +17,9 @@ import { getRulesSchemaMock } from '../../../../../../../common/api/detection_en import { DETECTION_ENGINE_RULES_URL } from '../../../../../../../common/constants'; import { updateRuleRoute } from './route'; import { + getCreateEqlRuleSchemaMock, + getCreateEsqlRulesSchemaMock, + getCreateNewTermsRulesSchemaMock, getCreateRulesSchemaMock, getUpdateRulesSchemaMock, } from '../../../../../../../common/api/detection_engine/model/rule_schema/mocks'; @@ -189,19 +192,29 @@ describe('Update rule route', () => { }); const defaultAction = getResponseAction(); - test('is successful', async () => { - const request = requestMock.create({ - method: 'post', - path: DETECTION_ENGINE_RULES_URL, - body: { - ...getCreateRulesSchemaMock(), - response_actions: [defaultAction], - }, - }); + const ruleTypes: Array<[string, () => object]> = [ + ['query', () => getCreateRulesSchemaMock()], + ['esql', getCreateEsqlRulesSchemaMock], + ['eql', getCreateEqlRuleSchemaMock], + ['new_terms', getCreateNewTermsRulesSchemaMock], + ]; - const response = await server.inject(request, requestContextMock.convertContext(context)); - expect(response.status).toEqual(200); - }); + test.each(ruleTypes)( + 'is successful for %s rule', + async (ruleType: string, schemaMock: (ruleId: string) => object) => { + const request = requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_RULES_URL, + body: { + ...schemaMock(`rule-${ruleType}`), + response_actions: [defaultAction], + }, + }); + + const response = await server.inject(request, requestContextMock.convertContext(context)); + expect(response.status).toEqual(200); + } + ); test('fails when isolate rbac is set to false', async () => { (context.securitySolution.getEndpointAuthz as jest.Mock).mockReturnValue(() => ({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts index 8a2609b712c53..2348c11027c65 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/convert_rule_response_to_alerting_rule.ts @@ -119,6 +119,9 @@ const typeSpecificSnakeToCamel = (params: TypeSpecificCreateProps): TypeSpecific eventCategoryOverride: params.event_category_override, tiebreakerField: params.tiebreaker_field, alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + responseActions: params.response_actions?.map((rule) => + transformRuleToAlertResponseAction(rule) + ), }; } case 'esql': { @@ -127,6 +130,9 @@ const typeSpecificSnakeToCamel = (params: TypeSpecificCreateProps): TypeSpecific language: params.language, query: params.query, alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + responseActions: params.response_actions?.map((rule) => + transformRuleToAlertResponseAction(rule) + ), }; } case 'threat_match': { @@ -173,9 +179,6 @@ const typeSpecificSnakeToCamel = (params: TypeSpecificCreateProps): TypeSpecific filters: params.filters, savedId: params.saved_id, dataViewId: params.data_view_id, - responseActions: params.response_actions?.map((rule) => - transformRuleToAlertResponseAction(rule) - ), alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), }; } @@ -213,6 +216,9 @@ const typeSpecificSnakeToCamel = (params: TypeSpecificCreateProps): TypeSpecific language: params.language ?? 'kuery', dataViewId: params.data_view_id, alertSuppression: convertObjectKeysToCamelCase(params.alert_suppression), + responseActions: params.response_actions?.map((rule) => + transformRuleToAlertResponseAction(rule) + ), }; } default: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts index 0808d1921e9bf..a4b74e31ba291 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/converters/type_specific_camel_to_snake.ts @@ -6,8 +6,8 @@ */ import type { RequiredOptional } from '@kbn/zod-helpers'; -import type { TypeSpecificResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; import { transformAlertToRuleResponseAction } from '../../../../../../../common/detection_engine/transform_actions'; +import type { TypeSpecificResponse } from '../../../../../../../common/api/detection_engine/model/rule_schema'; import { assertUnreachable } from '../../../../../../../common/utility_types'; import { convertObjectKeysToSnakeCase } from '../../../../../../utils/object_case_converters'; import type { TypeSpecificRuleParams } from '../../../../rule_schema'; @@ -28,6 +28,7 @@ export const typeSpecificCamelToSnake = ( event_category_override: params.eventCategoryOverride, tiebreaker_field: params.tiebreakerField, alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), }; } case 'esql': { @@ -36,6 +37,7 @@ export const typeSpecificCamelToSnake = ( language: params.language, query: params.query, alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), }; } case 'threat_match': { @@ -118,6 +120,7 @@ export const typeSpecificCamelToSnake = ( language: params.language, data_view_id: params.dataViewId, alert_suppression: convertObjectKeysToSnakeCase(params.alertSuppression), + response_actions: params.responseActions?.map(transformAlertToRuleResponseAction), }; } default: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts index 0263a60ab44ad..388b1ab695269 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_defaults.ts @@ -86,6 +86,7 @@ export const setTypeSpecificDefaults = (props: TypeSpecificCreateProps) => { event_category_override: props.event_category_override, tiebreaker_field: props.tiebreaker_field, alert_suppression: props.alert_suppression, + response_actions: props.response_actions, }; } case 'esql': { @@ -94,6 +95,7 @@ export const setTypeSpecificDefaults = (props: TypeSpecificCreateProps) => { language: props.language, query: props.query, alert_suppression: props.alert_suppression, + response_actions: props.response_actions, }; } case 'threat_match': { @@ -176,6 +178,7 @@ export const setTypeSpecificDefaults = (props: TypeSpecificCreateProps) => { language: props.language ?? 'kuery', data_view_id: props.data_view_id, alert_suppression: props.alert_suppression, + response_actions: props.response_actions, }; } default: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts index a8beef1bf2a0e..d864170746ed3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/detection_rules_client/mergers/apply_rule_patch.ts @@ -138,6 +138,7 @@ const patchEqlParams = ( rulePatch.event_category_override ?? existingRule.event_category_override, tiebreaker_field: rulePatch.tiebreaker_field ?? existingRule.tiebreaker_field, alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, + response_actions: rulePatch.response_actions ?? existingRule.response_actions, }; }; @@ -150,6 +151,7 @@ const patchEsqlParams = ( language: rulePatch.language ?? existingRule.language, query: rulePatch.query ?? existingRule.query, alert_suppression: rulePatch.alert_suppression ?? existingRule.alert_suppression, + response_actions: rulePatch.response_actions ?? existingRule.response_actions, }; }; @@ -258,6 +260,7 @@ const patchNewTermsParams = ( new_terms_fields: params.new_terms_fields ?? existingRule.new_terms_fields, history_window_start: params.history_window_start ?? existingRule.history_window_start, alert_suppression: params.alert_suppression ?? existingRule.alert_suppression, + response_actions: params.response_actions ?? existingRule.response_actions, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts index 500db54acd867..1274a2d7e7cad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.ts @@ -9,8 +9,13 @@ import type { PartialRule } from '@kbn/alerting-plugin/server'; import type { Rule } from '@kbn/alerting-plugin/common'; import { isEqual, xorWith } from 'lodash'; import { stringifyZodError } from '@kbn/zod-helpers'; +import type { + EqlRule, + EsqlRule, + NewTermsRule, + QueryRule, +} from '../../../../../common/api/detection_engine'; import { - type QueryRule, type ResponseAction, type RuleCreateProps, RuleResponse, @@ -21,9 +26,10 @@ import { RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP, RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ, } from '../../../../../common/endpoint/service/response_actions/constants'; -import { isQueryRule } from '../../../../../common/detection_engine/utils'; +import { shouldShowResponseActions } from '../../../../../common/detection_engine/utils'; import type { SecuritySolutionApiRequestHandlerContext } from '../../../..'; import { CustomHttpRequestError } from '../../../../utils/custom_http_request_error'; +import type { EqlRuleParams, EsqlRuleParams, NewTermsRuleParams } from '../../rule_schema'; import { hasValidRuleType, type RuleAlertType, @@ -64,11 +70,21 @@ export const validateResponseActionsPermissions = async ( ruleUpdate: RuleCreateProps | RuleUpdateProps, existingRule?: RuleAlertType | null ): Promise => { - if (!isQueryRule(ruleUpdate.type)) { + const { experimentalFeatures } = await securitySolution.getConfig(); + + if ( + !shouldShowResponseActions( + ruleUpdate.type, + experimentalFeatures.automatedResponseActionsForMoreRulesEnabled + ) + ) { return; } - if (!isQueryRulePayload(ruleUpdate) || (existingRule && !isQueryRuleObject(existingRule))) { + if ( + !rulePayloadContainsResponseActions(ruleUpdate) || + (existingRule && !ruleObjectContainsResponseActions(existingRule)) + ) { return; } @@ -108,10 +124,14 @@ export const validateResponseActionsPermissions = async ( }); }; -function isQueryRulePayload(rule: RuleCreateProps | RuleUpdateProps): rule is QueryRule { +function rulePayloadContainsResponseActions( + rule: RuleCreateProps | RuleUpdateProps +): rule is QueryRule | EsqlRule | EqlRule | NewTermsRule { return 'response_actions' in rule; } -function isQueryRuleObject(rule?: RuleAlertType): rule is Rule { +function ruleObjectContainsResponseActions( + rule?: RuleAlertType +): rule is Rule { return rule != null && 'params' in rule && 'responseActions' in rule?.params; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/endpoint_response_action.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/endpoint_response_action.ts index a310cb33497e8..040433789ecd7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/endpoint_response_action.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/endpoint_response_action.ts @@ -6,7 +6,6 @@ */ import { each } from 'lodash'; -import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; import { stringify } from '../../../endpoint/utils/stringify'; import type { RuleResponseEndpointAction, @@ -29,8 +28,8 @@ export const endpointResponseAction = async ( 'ruleExecution', 'automatedResponseActions' ); - const ruleId = alerts[0][ALERT_RULE_UUID]; - const ruleName = alerts[0][ALERT_RULE_NAME]; + const ruleId = alerts[0].kibana.alert?.rule.uuid; + const ruleName = alerts[0].kibana.alert?.rule.name; const logMsgPrefix = `Rule [${ruleName}][${ruleId}]:`; const { comment, command } = responseAction.params; const errors: string[] = []; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.test.ts index 4dccc9ad0aae7..d98dc0782b796 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.test.ts @@ -96,8 +96,13 @@ describe('ScheduleNotificationResponseActions', () => { }, }, ]; - await scheduleNotificationResponseActions({ signals, responseActions }); + const response = await scheduleNotificationResponseActions({ + signals, + signalsCount: signals.length, + responseActions, + }); + expect(response).not.toBeUndefined(); expect(osqueryActionMock.create).toHaveBeenCalledWith({ ...defaultQueryResultParams, query: simpleQuery, @@ -123,8 +128,13 @@ describe('ScheduleNotificationResponseActions', () => { }, }, ]; - await scheduleNotificationResponseActions({ signals, responseActions }); + const response = await scheduleNotificationResponseActions({ + signals, + signalsCount: signals.length, + responseActions, + }); + expect(response).not.toBeUndefined(); expect(osqueryActionMock.create).toHaveBeenCalledWith({ ...defaultPackResultParams, queries: [{ ...defaultQueries, id: 'query-1', query: simpleQuery }], @@ -149,8 +159,12 @@ describe('ScheduleNotificationResponseActions', () => { }, }, ]; - await scheduleNotificationResponseActions({ signals, responseActions }); - + const response = await scheduleNotificationResponseActions({ + signals, + signalsCount: signals.length, + responseActions, + }); + expect(response).not.toBeUndefined(); expect(endpointActionMock.getInternalResponseActionsClient).toHaveBeenCalledTimes(1); expect(endpointActionMock.getInternalResponseActionsClient).toHaveBeenCalledWith({ agentType: 'endpoint', @@ -188,11 +202,14 @@ describe('ScheduleNotificationResponseActions', () => { }, }, ]; - await scheduleNotificationResponseActions({ + const response = await scheduleNotificationResponseActions({ signals, + signalsCount: signals.length, responseActions, }); + expect(response).not.toBeUndefined(); + expect(mockedResponseActionsClient.killProcess).toHaveBeenCalledWith( { alert_ids: ['alert-id-1'], @@ -223,12 +240,42 @@ describe('ScheduleNotificationResponseActions', () => { }, }, ]; - await scheduleNotificationResponseActions({ + const response = await scheduleNotificationResponseActions({ signals, + signalsCount: signals.length, responseActions, }); + expect(response).not.toBeUndefined(); expect(mockedResponseActionsClient.isolate).toHaveBeenCalledTimes(signals.length - 1); }); + it('should not call any action service if no response actions are provided', async () => { + const response = await scheduleNotificationResponseActions({ + signals: getSignals(), + signalsCount: 2, + responseActions: [], + }); + expect(response).toBeUndefined(); + }); + it('should not call any action service if signalsCount is 0', async () => { + const signals = getSignals(); + const responseActions: RuleResponseAction[] = [ + { + actionTypeId: ResponseActionTypesEnum['.endpoint'], + params: { + command: 'isolate', + comment: 'test process comment', + }, + }, + ]; + + const response = await scheduleNotificationResponseActions({ + signals, + signalsCount: 0, + responseActions, + }); + + expect(response).toBeUndefined(); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts index 2fcf09d6cfbb4..b4f4689fed0ff 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/schedule_notification_response_actions.ts @@ -5,13 +5,14 @@ * 2.0. */ +import { expandDottedObject } from '../../../../common/utils/expand_dotted'; import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services'; import type { SetupPlugins } from '../../../plugin_contract'; import { ResponseActionTypesEnum } from '../../../../common/api/detection_engine/model/rule_response_actions'; import { osqueryResponseAction } from './osquery_response_action'; import { endpointResponseAction } from './endpoint_response_action'; import type { ScheduleNotificationActions } from '../rule_types/types'; -import type { AlertWithAgent, Alert } from './types'; +import type { Alert, AlertWithAgent } from './types'; interface ScheduleNotificationResponseActionsService { endpointAppContextService: EndpointAppContextService; @@ -23,10 +24,15 @@ export const getScheduleNotificationResponseActionsService = osqueryCreateActionService, endpointAppContextService, }: ScheduleNotificationResponseActionsService) => - async ({ signals, responseActions }: ScheduleNotificationActions) => { - const alerts = (signals as Alert[]).filter((alert) => alert.agent?.id) as AlertWithAgent[]; + async ({ signals, signalsCount, responseActions }: ScheduleNotificationActions) => { + if (!signalsCount || !responseActions?.length) { + return; + } + // expandDottedObject is needed eg in ESQL rule because it's alerts come without nested agent, host etc data but everything is dotted + const nestedAlerts = signals.map((signal) => expandDottedObject(signal as object)) as Alert[]; + const alerts = nestedAlerts.filter((alert) => alert.agent?.id) as AlertWithAgent[]; - await Promise.all( + return Promise.all( responseActions.map(async (responseAction) => { if ( responseAction.actionTypeId === ResponseActionTypesEnum['.osquery'] && diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/types.ts index e7317acfd7ca1..a72e813dcb6a7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions/types.ts @@ -19,6 +19,14 @@ export type Alert = ParsedTechnicalFields & { process?: { pid: string; }; + kibana: { + alert?: { + rule: { + uuid: string; + name: string; + }; + }; + }; }; export interface AlertAgent { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts index 632649f733473..e651ffeebaf49 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts @@ -162,6 +162,7 @@ export const EqlSpecificRuleParams = z.object({ timestampField: TimestampField.optional(), tiebreakerField: TiebreakerField.optional(), alertSuppression: AlertSuppressionCamel.optional(), + responseActions: z.array(RuleResponseAction).optional(), }); export type EqlRuleParams = BaseRuleParams & EqlSpecificRuleParams; @@ -173,6 +174,7 @@ export const EsqlSpecificRuleParams = z.object({ language: z.literal('esql'), query: RuleQuery, alertSuppression: AlertSuppressionCamel.optional(), + responseActions: z.array(RuleResponseAction).optional(), }); export type EsqlRuleParams = BaseRuleParams & EsqlSpecificRuleParams; @@ -280,6 +282,7 @@ export const NewTermsSpecificRuleParams = z.object({ language: KqlQueryLanguage, dataViewId: DataViewId.optional(), alertSuppression: AlertSuppressionCamel.optional(), + responseActions: z.array(RuleResponseAction).optional(), }); export type NewTermsRuleParams = BaseRuleParams & NewTermsSpecificRuleParams; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts index 81971feeecfc1..ca16b38404e48 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts @@ -11,16 +11,22 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { SERVER_APP_ID } from '../../../../../common/constants'; import { EqlRuleParams } from '../../rule_schema'; import { eqlExecutor } from './eql'; -import type { CreateRuleOptions, SecurityAlertType, SignalSourceHit } from '../types'; +import type { + CreateRuleOptions, + SecurityAlertType, + SignalSourceHit, + CreateRuleAdditionalOptions, +} from '../types'; import { validateIndexPatterns } from '../utils'; import type { BuildReasonMessage } from '../utils/reason_formatters'; import { wrapSuppressedAlerts } from '../utils/wrap_suppressed_alerts'; import { getIsAlertSuppressionActive } from '../utils/get_is_alert_suppression_active'; export const createEqlAlertType = ( - createOptions: CreateRuleOptions + createOptions: CreateRuleOptions & CreateRuleAdditionalOptions ): SecurityAlertType => { - const { experimentalFeatures, version, licensing } = createOptions; + const { experimentalFeatures, version, licensing, scheduleNotificationResponseActionsService } = + createOptions; return { id: EQL_RULE_TYPE_ID, name: 'Event Correlation Rule', @@ -125,6 +131,7 @@ export const createEqlAlertType = ( alertWithSuppression, isAlertSuppressionActive: isNonSeqAlertSuppressionActive, experimentalFeatures, + scheduleNotificationResponseActionsService, }); return { ...result, state }; }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts index c16d61d3b0ea5..9ef9faeb9de3a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts @@ -37,6 +37,7 @@ describe('eql_executor', () => { maxSignals: params.maxSignals, }; const mockExperimentalFeatures = {} as ExperimentalFeatures; + const mockScheduleNotificationResponseActionsService = jest.fn(); beforeEach(() => { jest.clearAllMocks(); @@ -72,6 +73,8 @@ describe('eql_executor', () => { alertWithSuppression: jest.fn(), isAlertSuppressionActive: false, experimentalFeatures: mockExperimentalFeatures, + scheduleNotificationResponseActionsService: + mockScheduleNotificationResponseActionsService, }); expect(result.warningMessages).toEqual([ `The following exceptions won't be applied to rule execution: ${ @@ -121,6 +124,8 @@ describe('eql_executor', () => { alertWithSuppression: jest.fn(), isAlertSuppressionActive: true, experimentalFeatures: mockExperimentalFeatures, + scheduleNotificationResponseActionsService: + mockScheduleNotificationResponseActionsService, }); expect(result.warningMessages).toContain( @@ -154,10 +159,40 @@ describe('eql_executor', () => { alertWithSuppression: jest.fn(), isAlertSuppressionActive: true, experimentalFeatures: mockExperimentalFeatures, + scheduleNotificationResponseActionsService: mockScheduleNotificationResponseActionsService, }); expect(result.userError).toEqual(true); }); + it('should handle scheduleNotificationResponseActionsService call', async () => { + const result = await eqlExecutor({ + inputIndex: DEFAULT_INDEX_PATTERN, + runtimeMappings: {}, + completeRule: eqlCompleteRule, + tuple, + ruleExecutionLogger, + services: alertServices, + version, + bulkCreate: jest.fn(), + wrapHits: jest.fn(), + wrapSequences: jest.fn(), + primaryTimestamp: '@timestamp', + exceptionFilter: undefined, + unprocessedExceptions: [], + wrapSuppressedHits: jest.fn(), + alertTimestampOverride: undefined, + alertWithSuppression: jest.fn(), + isAlertSuppressionActive: false, + experimentalFeatures: mockExperimentalFeatures, + scheduleNotificationResponseActionsService: mockScheduleNotificationResponseActionsService, + }); + expect(mockScheduleNotificationResponseActionsService).toBeCalledWith({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: eqlCompleteRule.ruleParams.responseActions, + }); + }); + it('should pass frozen tier filters in eql search request', async () => { getDataTierFilterMock.mockResolvedValue([ { @@ -189,6 +224,7 @@ describe('eql_executor', () => { alertWithSuppression: jest.fn(), isAlertSuppressionActive: true, experimentalFeatures: mockExperimentalFeatures, + scheduleNotificationResponseActionsService: mockScheduleNotificationResponseActionsService, }); const searchArgs = diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts index a3a1ba545c0ea..3379d0a0c6867 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts @@ -26,6 +26,7 @@ import type { SearchAfterAndBulkCreateReturnType, SignalSource, WrapSuppressedHits, + CreateRuleAdditionalOptions, } from '../types'; import { addToSearchAfterReturn, @@ -66,6 +67,7 @@ interface EqlExecutorParams { alertWithSuppression: SuppressedAlertService; isAlertSuppressionActive: boolean; experimentalFeatures: ExperimentalFeatures; + scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService']; } export const eqlExecutor = async ({ @@ -88,6 +90,7 @@ export const eqlExecutor = async ({ alertWithSuppression, isAlertSuppressionActive, experimentalFeatures, + scheduleNotificationResponseActionsService, }: EqlExecutorParams): Promise => { const ruleParams = completeRule.ruleParams; @@ -188,6 +191,14 @@ export const eqlExecutor = async ({ result.warningMessages.push(maxSignalsWarning); } + if (scheduleNotificationResponseActionsService) { + scheduleNotificationResponseActionsService({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: completeRule.ruleParams.responseActions, + }); + } + return result; } catch (error) { if ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/create_esql_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/create_esql_alert_type.ts index 10c82ad8fed7c..31afe8d2a191f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/create_esql_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/create_esql_alert_type.ts @@ -11,12 +11,13 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { SERVER_APP_ID } from '../../../../../common/constants'; import { EsqlRuleParams } from '../../rule_schema'; import { esqlExecutor } from './esql'; -import type { CreateRuleOptions, SecurityAlertType } from '../types'; +import type { CreateRuleOptions, SecurityAlertType, CreateRuleAdditionalOptions } from '../types'; export const createEsqlAlertType = ( - createOptions: CreateRuleOptions + createOptions: CreateRuleOptions & CreateRuleAdditionalOptions ): SecurityAlertType => { - const { version, experimentalFeatures, licensing } = createOptions; + const { version, experimentalFeatures, licensing, scheduleNotificationResponseActionsService } = + createOptions; return { id: ESQL_RULE_TYPE_ID, name: 'ES|QL Rule', @@ -44,6 +45,13 @@ export const createEsqlAlertType = ( isExportable: false, category: DEFAULT_APP_CATEGORIES.security.id, producer: SERVER_APP_ID, - executor: (params) => esqlExecutor({ ...params, experimentalFeatures, version, licensing }), + executor: (params) => + esqlExecutor({ + ...params, + experimentalFeatures, + version, + licensing, + scheduleNotificationResponseActionsService, + }), }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts index b129a7ef0c5bb..0dd2b0e50d4ba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts @@ -28,8 +28,7 @@ import { rowToDocument } from './utils'; import { fetchSourceDocuments } from './fetch_source_documents'; import { buildReasonMessageForEsqlAlert } from '../utils/reason_formatters'; -import type { RunOpts, SignalSource } from '../types'; - +import type { RunOpts, SignalSource, CreateRuleAdditionalOptions } from '../types'; import { addToSearchAfterReturn, createSearchAfterReturnType, @@ -63,6 +62,7 @@ export const esqlExecutor = async ({ spaceId, experimentalFeatures, licensing, + scheduleNotificationResponseActionsService, }: { runOpts: RunOpts; services: RuleExecutorServices; @@ -71,6 +71,7 @@ export const esqlExecutor = async ({ version: string; experimentalFeatures: ExperimentalFeatures; licensing: LicensingPluginSetup; + scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService']; }) => { const ruleParams = completeRule.ruleParams; /** @@ -225,6 +226,13 @@ export const esqlExecutor = async ({ break; } } + if (scheduleNotificationResponseActionsService) { + scheduleNotificationResponseActionsService({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: completeRule.ruleParams.responseActions, + }); + } // no more results will be found if (response.values.length < size) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts index 74c7d9437851e..e33f580388f98 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts @@ -12,7 +12,7 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { SERVER_APP_ID } from '../../../../../common/constants'; import { NewTermsRuleParams } from '../../rule_schema'; -import type { CreateRuleOptions, SecurityAlertType } from '../types'; +import type { CreateRuleOptions, SecurityAlertType, CreateRuleAdditionalOptions } from '../types'; import { singleSearchAfter } from '../utils/single_search_after'; import { getFilter } from '../utils/get_filter'; import { wrapNewTermsAlerts } from './wrap_new_terms_alerts'; @@ -46,9 +46,10 @@ import { multiTermsComposite } from './multi_terms_composite'; import type { GenericBulkCreateResponse } from '../utils/bulk_create_with_suppression'; export const createNewTermsAlertType = ( - createOptions: CreateRuleOptions + createOptions: CreateRuleOptions & CreateRuleAdditionalOptions ): SecurityAlertType => { - const { logger, licensing, experimentalFeatures } = createOptions; + const { logger, licensing, experimentalFeatures, scheduleNotificationResponseActionsService } = + createOptions; return { id: NEW_TERMS_RULE_TYPE_ID, name: 'New Terms Rule', @@ -414,6 +415,15 @@ export const createNewTermsAlertType = ( afterKey = searchResultWithAggs.aggregations.new_terms.after_key; } + + if (scheduleNotificationResponseActionsService) { + scheduleNotificationResponseActionsService({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: completeRule.ruleParams.responseActions, + }); + } + return { ...result, state }; }, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts index 272184dbf1e58..5915447e5a541 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts @@ -22,7 +22,7 @@ import type { UnifiedQueryRuleParams } from '../../rule_schema'; import type { ExperimentalFeatures } from '../../../../../common/experimental_features'; import { buildReasonMessageForQueryAlert } from '../utils/reason_formatters'; import { withSecuritySpan } from '../../../../utils/with_security_span'; -import type { CreateQueryRuleAdditionalOptions, RunOpts } from '../types'; +import type { CreateRuleAdditionalOptions, RunOpts } from '../types'; export const queryExecutor = async ({ runOpts, @@ -42,7 +42,7 @@ export const queryExecutor = async ({ version: string; spaceId: string; bucketHistory?: BucketHistory[]; - scheduleNotificationResponseActionsService?: CreateQueryRuleAdditionalOptions['scheduleNotificationResponseActionsService']; + scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService']; licensing: LicensingPluginSetup; }) => { const completeRule = runOpts.completeRule; @@ -99,13 +99,10 @@ export const queryExecutor = async ({ state: {}, }; - if ( - completeRule.ruleParams.responseActions?.length && - result.createdSignalsCount && - scheduleNotificationResponseActionsService - ) { + if (scheduleNotificationResponseActionsService) { scheduleNotificationResponseActionsService({ signals: result.createdSignals, + signalsCount: result.createdSignalsCount, responseActions: completeRule.ruleParams.responseActions, }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index 4069b7782e0e8..a29beef7bbb20 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -161,15 +161,15 @@ export interface CreateRuleOptions { export interface ScheduleNotificationActions { signals: unknown[]; - responseActions: RuleResponseAction[]; + signalsCount: number; + responseActions: RuleResponseAction[] | undefined; } -export interface CreateQueryRuleAdditionalOptions { + +export interface CreateRuleAdditionalOptions { scheduleNotificationResponseActionsService?: (params: ScheduleNotificationActions) => void; } -export interface CreateQueryRuleOptions - extends CreateRuleOptions, - CreateQueryRuleAdditionalOptions { +export interface CreateQueryRuleOptions extends CreateRuleOptions, CreateRuleAdditionalOptions { id: typeof QUERY_RULE_TYPE_ID | typeof SAVED_QUERY_RULE_TYPE_ID; name: 'Custom Query Rule' | 'Saved Query Rule'; } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index c24e70baa5db8..a46863c78c25e 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -78,7 +78,7 @@ import type { IRuleMonitoringService } from './lib/detection_engine/rule_monitor import { createRuleMonitoringService } from './lib/detection_engine/rule_monitoring'; import { EndpointMetadataService } from './endpoint/services/metadata'; import type { - CreateQueryRuleAdditionalOptions, + CreateRuleAdditionalOptions, CreateRuleOptions, } from './lib/detection_engine/rule_types/types'; // eslint-disable-next-line no-restricted-imports @@ -311,7 +311,7 @@ export class Plugin implements ISecuritySolutionPlugin { analytics: core.analytics, }; - const queryRuleAdditionalOptions: CreateQueryRuleAdditionalOptions = { + const ruleAdditionalOptions: CreateRuleAdditionalOptions = { scheduleNotificationResponseActionsService: getScheduleNotificationResponseActionsService({ endpointAppContextService: this.endpointAppContextService, osqueryCreateActionService: plugins.osquery.createActionService, @@ -320,15 +320,19 @@ export class Plugin implements ISecuritySolutionPlugin { const securityRuleTypeWrapper = createSecurityRuleTypeWrapper(securityRuleTypeOptions); - plugins.alerting.registerType(securityRuleTypeWrapper(createEqlAlertType(ruleOptions))); + plugins.alerting.registerType( + securityRuleTypeWrapper(createEqlAlertType({ ...ruleOptions, ...ruleAdditionalOptions })) + ); if (!experimentalFeatures.esqlRulesDisabled) { - plugins.alerting.registerType(securityRuleTypeWrapper(createEsqlAlertType(ruleOptions))); + plugins.alerting.registerType( + securityRuleTypeWrapper(createEsqlAlertType({ ...ruleOptions, ...ruleAdditionalOptions })) + ); } plugins.alerting.registerType( securityRuleTypeWrapper( createQueryAlertType({ ...ruleOptions, - ...queryRuleAdditionalOptions, + ...ruleAdditionalOptions, id: SAVED_QUERY_RULE_TYPE_ID, name: 'Saved Query Rule', }) @@ -342,14 +346,16 @@ export class Plugin implements ISecuritySolutionPlugin { securityRuleTypeWrapper( createQueryAlertType({ ...ruleOptions, - ...queryRuleAdditionalOptions, + ...ruleAdditionalOptions, id: QUERY_RULE_TYPE_ID, name: 'Custom Query Rule', }) ) ); plugins.alerting.registerType(securityRuleTypeWrapper(createThresholdAlertType(ruleOptions))); - plugins.alerting.registerType(securityRuleTypeWrapper(createNewTermsAlertType(ruleOptions))); + plugins.alerting.registerType( + securityRuleTypeWrapper(createNewTermsAlertType({ ...ruleOptions, ...ruleAdditionalOptions })) + ); // TODO We need to get the endpoint routes inside of initRoutes initRoutes(