From 60176bcffdcbdb75b48823f4783923528797efe0 Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:45:41 +0100 Subject: [PATCH] [Security Solution][Detection Engine] log ES requests when running rule preview (#191107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary **Status:** works only for **ES|QL and EQL** rule types When clicking on "Show Elasticsearch requests, ran during rule executions" preview would return logged Elasticsearch queries that can be used to debug/explore rule execution. Each rule execution accordion has time rule execution started and its duration. Upon opening accordion: it will display ES requests with their description and duration. **NOTE**: Only search requests are returned, not the requests that create actual alerts Feature flag: **loggingRequestsEnabled** On week Demo([internal link](https://drive.google.com/drive/folders/1l-cDhbiMxykNH6BzIxFAnLeibmV9a4Cz)) ### Video demo (older UI) https://github.com/user-attachments/assets/26f963da-c528-447c-9efd-350b4d42b52c ### Up to date UI #### UI control Screenshot 2024-09-11 at 12 39 07 #### List of executions and code blocks Screenshot 2024-09-11 at 12 38 23 ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed 🎉 All tests passed! - [kibana-flaky-test-suite-runner#6909](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6909) [✅] [Serverless] Security Solution Detection Engine - Cypress: 100/100 tests passed. [✅] Security Solution Detection Engine - Cypress: 100/100 tests passed. FTR tests - https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6918 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../rule_preview/rule_preview.gen.ts | 18 ++ .../rule_preview/rule_preview.schema.yaml | 23 ++ .../common/api/quickstart_client.gen.ts | 3 + .../common/experimental_features.ts | 5 + ...ections_api_2023_10_31.bundled.schema.yaml | 24 ++ ...ections_api_2023_10_31.bundled.schema.yaml | 24 ++ .../rule_preview/__mocks__/preview_logs.ts | 93 ++++++ .../components/rule_preview/index.test.tsx | 68 ++++- .../components/rule_preview/index.tsx | 48 ++- .../rule_preview/logged_requests.test.tsx | 68 +++++ .../rule_preview/logged_requests.tsx | 58 ++++ .../rule_preview/logged_requests_item.tsx | 79 +++++ .../rule_preview/optimized_accordion.test.tsx | 59 ++++ .../rule_preview/optimized_accordion.tsx | 39 +++ .../components/rule_preview/preview_logs.tsx | 20 +- .../components/rule_preview/translations.ts | 21 ++ .../rule_preview/use_accordion_styling.ts | 16 + .../rule_preview/use_preview_route.tsx | 3 + .../rule_preview/use_preview_rule.ts | 5 +- .../rule_management/api/api.test.ts | 15 + .../rule_management/api/api.ts | 2 + .../rule_management/logic/types.ts | 6 +- .../rule_preview/api/preview_rules/route.ts | 23 +- .../create_security_rule_type_wrapper.ts | 6 +- .../rule_types/eql/create_eql_alert_type.ts | 5 +- .../rule_types/eql/eql.test.ts | 8 +- .../detection_engine/rule_types/eql/eql.ts | 33 ++- .../detection_engine/rule_types/esql/esql.ts | 279 ++++++++++-------- .../rule_types/esql/fetch_source_documents.ts | 31 +- .../rule_types/translations.ts | 29 ++ .../lib/detection_engine/rule_types/types.ts | 9 +- .../rule_types/utils/logged_requests/index.ts | 10 + .../utils/logged_requests/log_eql.ts | 19 ++ .../utils/logged_requests/log_esql.ts | 15 + .../utils/logged_requests/log_query.ts | 35 +++ .../services/security_solution_api.gen.ts | 9 +- .../config/ess/config.base.ts | 1 + .../configs/serverless.config.ts | 5 +- .../execution_logic/eql.ts | 27 ++ .../execution_logic/esql.ts | 58 ++++ .../utils/rules/preview_rule.ts | 3 + .../test/security_solution_cypress/config.ts | 5 +- .../detection_engine/rule_edit/preview.cy.ts | 85 ++++++ .../cypress/screens/create_new_rule.ts | 16 + .../cypress/tasks/create_new_rule.ts | 20 ++ .../serverless_config.ts | 5 +- 46 files changed, 1268 insertions(+), 165 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/__mocks__/preview_logs.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests_item.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_accordion_styling.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/translations.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/index.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_eql.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_esql.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_query.ts create mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts index 0e7fb75c2c4c2..ad9b6d9ea12c2 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts @@ -15,6 +15,7 @@ */ import { z } from '@kbn/zod'; +import { BooleanFromString } from '@kbn/zod-helpers'; import { EqlRuleCreateProps, @@ -34,6 +35,13 @@ export const RulePreviewParams = z.object({ timeframeEnd: z.string().datetime(), }); +export type RulePreviewLoggedRequest = z.infer; +export const RulePreviewLoggedRequest = z.object({ + request: NonEmptyString, + description: NonEmptyString.optional(), + duration: z.number().int().optional(), +}); + export type RulePreviewLogs = z.infer; export const RulePreviewLogs = z.object({ errors: z.array(NonEmptyString), @@ -43,7 +51,17 @@ export const RulePreviewLogs = z.object({ */ duration: z.number().int(), startedAt: NonEmptyString.optional(), + requests: z.array(RulePreviewLoggedRequest).optional(), +}); + +export type RulePreviewRequestQuery = z.infer; +export const RulePreviewRequestQuery = z.object({ + /** + * Enables logging and returning in response ES queries, performed during rule execution + */ + enable_logged_requests: BooleanFromString.optional(), }); +export type RulePreviewRequestQueryInput = z.input; export type RulePreviewRequestBody = z.infer; export const RulePreviewRequestBody = z.discriminatedUnion('type', [ diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml index 1d65d6b4e037e..400b84e533a02 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml @@ -11,6 +11,13 @@ paths: summary: Preview rule alerts generated on specified time range tags: - Rule preview API + parameters: + - name: enable_logged_requests + in: query + description: Enables logging and returning in response ES queries, performed during rule execution + required: false + schema: + type: boolean requestBody: description: An object containing tags to add or remove and alert ids the changes will be applied required: true @@ -94,6 +101,18 @@ components: format: date-time required: [invocationCount, timeframeEnd] + RulePreviewLoggedRequest: + type: object + properties: + request: + $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' + description: + $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' + duration: + type: integer + required: + - request + RulePreviewLogs: type: object properties: @@ -110,6 +129,10 @@ components: description: Execution duration in milliseconds startedAt: $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' + requests: + type: array + items: + $ref: '#/components/schemas/RulePreviewLoggedRequest' required: - errors - warnings diff --git a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts index 5c9d33858d0f7..bb564dbe69b34 100644 --- a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -99,6 +99,7 @@ import type { GetRuleExecutionResultsResponse, } from './detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.gen'; import type { + RulePreviewRequestQueryInput, RulePreviewRequestBodyInput, RulePreviewResponse, } from './detection_engine/rule_preview/rule_preview.gen'; @@ -1763,6 +1764,7 @@ detection engine rules. }, method: 'POST', body: props.body, + query: props.query, }) .catch(catchAxiosErrorFormatAndThrow); } @@ -2160,6 +2162,7 @@ export interface ResolveTimelineProps { query: ResolveTimelineRequestQueryInput; } export interface RulePreviewProps { + query: RulePreviewRequestQueryInput; body: RulePreviewRequestBodyInput; } export interface SearchAlertsProps { diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 4147404e940c1..030e00768349d 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -138,6 +138,11 @@ export const allowedExperimentalValues = Object.freeze({ */ esqlRulesDisabled: false, + /** + * enables logging requests during rule preview + */ + loggingRequestsEnabled: false, + /** * Enables Protection Updates tab in the Endpoint Policy Details page */ 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 0df64f51f37ce..b9c1ac658fd90 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 @@ -891,6 +891,15 @@ paths: /api/detection_engine/rules/preview: post: operationId: RulePreview + parameters: + - description: >- + Enables logging and returning in response ES queries, performed + during rule execution + in: query + name: enable_logged_requests + required: false + schema: + type: boolean requestBody: content: application/json: @@ -5178,6 +5187,17 @@ components: - $ref: '#/components/schemas/MachineLearningRulePatchProps' - $ref: '#/components/schemas/NewTermsRulePatchProps' - $ref: '#/components/schemas/EsqlRulePatchProps' + RulePreviewLoggedRequest: + type: object + properties: + description: + $ref: '#/components/schemas/NonEmptyString' + duration: + type: integer + request: + $ref: '#/components/schemas/NonEmptyString' + required: + - request RulePreviewLogs: type: object properties: @@ -5188,6 +5208,10 @@ components: items: $ref: '#/components/schemas/NonEmptyString' type: array + requests: + items: + $ref: '#/components/schemas/RulePreviewLoggedRequest' + type: array startedAt: $ref: '#/components/schemas/NonEmptyString' warnings: 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 4699de9a25228..d1de42913c4e0 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 @@ -476,6 +476,15 @@ paths: /api/detection_engine/rules/preview: post: operationId: RulePreview + parameters: + - description: >- + Enables logging and returning in response ES queries, performed + during rule execution + in: query + name: enable_logged_requests + required: false + schema: + type: boolean requestBody: content: application/json: @@ -4331,6 +4340,17 @@ components: - $ref: '#/components/schemas/MachineLearningRulePatchProps' - $ref: '#/components/schemas/NewTermsRulePatchProps' - $ref: '#/components/schemas/EsqlRulePatchProps' + RulePreviewLoggedRequest: + type: object + properties: + description: + $ref: '#/components/schemas/NonEmptyString' + duration: + type: integer + request: + $ref: '#/components/schemas/NonEmptyString' + required: + - request RulePreviewLogs: type: object properties: @@ -4341,6 +4361,10 @@ components: items: $ref: '#/components/schemas/NonEmptyString' type: array + requests: + items: + $ref: '#/components/schemas/RulePreviewLoggedRequest' + type: array startedAt: $ref: '#/components/schemas/NonEmptyString' warnings: diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/__mocks__/preview_logs.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/__mocks__/preview_logs.ts new file mode 100644 index 0000000000000..1e380d1bb4561 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/__mocks__/preview_logs.ts @@ -0,0 +1,93 @@ +/* + * 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 { RulePreviewLogs } from '../../../../../../common/api/detection_engine'; + +export const previewLogs: RulePreviewLogs[] = [ + { + errors: [], + warnings: [], + startedAt: '2024-09-05T15:43:46.972Z', + duration: 149, + requests: [ + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 101",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T15:43:46.972Z",\n "gte": "2024-09-05T15:22:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + duration: 23, + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "yB7awpEBluhaSO8ejVKZ",\n "yR7awpEBluhaSO8ejVKZ",\n "yh7awpEBluhaSO8ejVKZ",\n "yx7awpEBluhaSO8ejVKZ",\n "zB7awpEBluhaSO8ejVKZ",\n "zR7awpEBluhaSO8ejVKZ",\n "zh7awpEBluhaSO8ejVKZ",\n "zx7awpEBluhaSO8ejVKZ",\n "0B7awpEBluhaSO8ejVKZ",\n "0R7awpEBluhaSO8ejVKZ",\n "0h7awpEBluhaSO8ejVKZ",\n "0x7awpEBluhaSO8ejVKZ",\n "1B7awpEBluhaSO8ejVKZ",\n "1R7awpEBluhaSO8ejVKZ",\n "1h7awpEBluhaSO8ejVKZ",\n "1x7awpEBluhaSO8ejVKZ",\n "2B7awpEBluhaSO8ejVKZ",\n "2R7awpEBluhaSO8ejVKZ",\n "2h7awpEBluhaSO8ejVKZ",\n "2x7awpEBluhaSO8ejVKZ",\n "3B7awpEBluhaSO8ejVKZ",\n "3R7awpEBluhaSO8ejVKZ",\n "3h7awpEBluhaSO8ejVKZ",\n "3x7awpEBluhaSO8ejVKZ",\n "4B7awpEBluhaSO8ejVKZ",\n "4R7awpEBluhaSO8ejVKZ",\n "4h7awpEBluhaSO8ejVKZ",\n "4x7awpEBluhaSO8ejVKZ",\n "5B7awpEBluhaSO8ejVKZ",\n "5R7awpEBluhaSO8ejVKZ",\n "5h7awpEBluhaSO8ejVKZ",\n "5x7awpEBluhaSO8ejVKZ",\n "6B7awpEBluhaSO8ejVKZ",\n "6R7awpEBluhaSO8ejVKZ",\n "6h7awpEBluhaSO8ejVKZ",\n "6x7awpEBluhaSO8ejVKZ",\n "7B7awpEBluhaSO8ejVKZ",\n "7R7awpEBluhaSO8ejVKZ",\n "7h7awpEBluhaSO8ejVKZ",\n "7x7awpEBluhaSO8ejVKZ",\n "8B7awpEBluhaSO8ejVKZ",\n "8R7awpEBluhaSO8ejVKZ",\n "8h7awpEBluhaSO8ejVKZ",\n "8x7awpEBluhaSO8ejVKZ",\n "9B7awpEBluhaSO8ejVKZ",\n "9R7awpEBluhaSO8ejVKZ",\n "9h7awpEBluhaSO8ejVKZ",\n "9x7awpEBluhaSO8ejVKZ",\n "-B7awpEBluhaSO8ejVKZ",\n "-R7awpEBluhaSO8ejVKZ",\n "-h7awpEBluhaSO8ejVKZ",\n "-x7awpEBluhaSO8ejVKZ",\n "_B7awpEBluhaSO8ejVKZ",\n "_R7awpEBluhaSO8ejVKZ",\n "_h7awpEBluhaSO8ejVKZ",\n "_x7awpEBluhaSO8ejVKZ",\n "AB7awpEBluhaSO8ejVOZ",\n "AR7awpEBluhaSO8ejVOZ",\n "Ah7awpEBluhaSO8ejVOZ",\n "Ax7awpEBluhaSO8ejVOZ",\n "BB7awpEBluhaSO8ejVOZ",\n "BR7awpEBluhaSO8ejVOZ",\n "Bh7awpEBluhaSO8ejVOZ",\n "Bx7awpEBluhaSO8ejVOZ",\n "CB7awpEBluhaSO8ejVOZ",\n "CR7awpEBluhaSO8ejVOZ",\n "Ch7awpEBluhaSO8ejVOZ",\n "Cx7awpEBluhaSO8ejVOZ",\n "DB7awpEBluhaSO8ejVOZ",\n "DR7awpEBluhaSO8ejVOZ",\n "Dh7awpEBluhaSO8ejVOZ",\n "Dx7awpEBluhaSO8ejVOZ",\n "EB7awpEBluhaSO8ejVOZ",\n "ER7awpEBluhaSO8ejVOZ",\n "Eh7awpEBluhaSO8ejVOZ",\n "Ex7awpEBluhaSO8ejVOZ",\n "FB7awpEBluhaSO8ejVOZ",\n "FR7awpEBluhaSO8ejVOZ",\n "Fh7awpEBluhaSO8ejVOZ",\n "Fx7awpEBluhaSO8ejVOZ",\n "GB7awpEBluhaSO8ejVOZ",\n "GR7awpEBluhaSO8ejVOZ",\n "Gh7awpEBluhaSO8ejVOZ",\n "Gx7awpEBluhaSO8ejVOZ",\n "HB7awpEBluhaSO8ejVOZ",\n "HR7awpEBluhaSO8ejVOZ",\n "Hh7awpEBluhaSO8ejVOZ",\n "Hx7awpEBluhaSO8ejVOZ",\n "IB7awpEBluhaSO8ejVOZ",\n "IR7awpEBluhaSO8ejVOZ",\n "Ih7awpEBluhaSO8ejVOZ",\n "Ix7awpEBluhaSO8ejVOZ",\n "JB7awpEBluhaSO8ejVOZ",\n "JR7awpEBluhaSO8ejVOZ",\n "Jh7awpEBluhaSO8ejVOZ",\n "Jx7awpEBluhaSO8ejVOZ",\n "KB7awpEBluhaSO8ejVOZ",\n "KR7awpEBluhaSO8ejVOZ",\n "Kh7awpEBluhaSO8ejVOZ",\n "Kx7awpEBluhaSO8ejVOZ",\n "LB7awpEBluhaSO8ejVOZ"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 8, + }, + ], + }, + { + errors: [], + warnings: [], + startedAt: '2024-09-05T16:03:46.972Z', + duration: 269, + requests: [ + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 101",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T16:03:46.972Z",\n "gte": "2024-09-05T15:42:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + duration: 30, + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "yB7awpEBluhaSO8ejVKZ",\n "yR7awpEBluhaSO8ejVKZ",\n "yh7awpEBluhaSO8ejVKZ",\n "yx7awpEBluhaSO8ejVKZ",\n "zB7awpEBluhaSO8ejVKZ",\n "zR7awpEBluhaSO8ejVKZ",\n "zh7awpEBluhaSO8ejVKZ",\n "zx7awpEBluhaSO8ejVKZ",\n "0B7awpEBluhaSO8ejVKZ",\n "0R7awpEBluhaSO8ejVKZ",\n "0h7awpEBluhaSO8ejVKZ",\n "0x7awpEBluhaSO8ejVKZ",\n "1B7awpEBluhaSO8ejVKZ",\n "1R7awpEBluhaSO8ejVKZ",\n "1h7awpEBluhaSO8ejVKZ",\n "1x7awpEBluhaSO8ejVKZ",\n "2B7awpEBluhaSO8ejVKZ",\n "2R7awpEBluhaSO8ejVKZ",\n "2h7awpEBluhaSO8ejVKZ",\n "2x7awpEBluhaSO8ejVKZ",\n "3B7awpEBluhaSO8ejVKZ",\n "3R7awpEBluhaSO8ejVKZ",\n "3h7awpEBluhaSO8ejVKZ",\n "3x7awpEBluhaSO8ejVKZ",\n "4B7awpEBluhaSO8ejVKZ",\n "4R7awpEBluhaSO8ejVKZ",\n "4h7awpEBluhaSO8ejVKZ",\n "4x7awpEBluhaSO8ejVKZ",\n "5B7awpEBluhaSO8ejVKZ",\n "5R7awpEBluhaSO8ejVKZ",\n "5h7awpEBluhaSO8ejVKZ",\n "5x7awpEBluhaSO8ejVKZ",\n "6B7awpEBluhaSO8ejVKZ",\n "6R7awpEBluhaSO8ejVKZ",\n "6h7awpEBluhaSO8ejVKZ",\n "6x7awpEBluhaSO8ejVKZ",\n "7B7awpEBluhaSO8ejVKZ",\n "7R7awpEBluhaSO8ejVKZ",\n "7h7awpEBluhaSO8ejVKZ",\n "7x7awpEBluhaSO8ejVKZ",\n "8B7awpEBluhaSO8ejVKZ",\n "8R7awpEBluhaSO8ejVKZ",\n "8h7awpEBluhaSO8ejVKZ",\n "8x7awpEBluhaSO8ejVKZ",\n "9B7awpEBluhaSO8ejVKZ",\n "9R7awpEBluhaSO8ejVKZ",\n "9h7awpEBluhaSO8ejVKZ",\n "9x7awpEBluhaSO8ejVKZ",\n "-B7awpEBluhaSO8ejVKZ",\n "-R7awpEBluhaSO8ejVKZ",\n "-h7awpEBluhaSO8ejVKZ",\n "-x7awpEBluhaSO8ejVKZ",\n "_B7awpEBluhaSO8ejVKZ",\n "_R7awpEBluhaSO8ejVKZ",\n "_h7awpEBluhaSO8ejVKZ",\n "_x7awpEBluhaSO8ejVKZ",\n "AB7awpEBluhaSO8ejVOZ",\n "AR7awpEBluhaSO8ejVOZ",\n "Ah7awpEBluhaSO8ejVOZ",\n "Ax7awpEBluhaSO8ejVOZ",\n "BB7awpEBluhaSO8ejVOZ",\n "BR7awpEBluhaSO8ejVOZ",\n "Bh7awpEBluhaSO8ejVOZ",\n "Bx7awpEBluhaSO8ejVOZ",\n "CB7awpEBluhaSO8ejVOZ",\n "CR7awpEBluhaSO8ejVOZ",\n "Ch7awpEBluhaSO8ejVOZ",\n "Cx7awpEBluhaSO8ejVOZ",\n "DB7awpEBluhaSO8ejVOZ",\n "DR7awpEBluhaSO8ejVOZ",\n "Dh7awpEBluhaSO8ejVOZ",\n "Dx7awpEBluhaSO8ejVOZ",\n "EB7awpEBluhaSO8ejVOZ",\n "ER7awpEBluhaSO8ejVOZ",\n "Eh7awpEBluhaSO8ejVOZ",\n "Ex7awpEBluhaSO8ejVOZ",\n "FB7awpEBluhaSO8ejVOZ",\n "FR7awpEBluhaSO8ejVOZ",\n "Fh7awpEBluhaSO8ejVOZ",\n "Fx7awpEBluhaSO8ejVOZ",\n "GB7awpEBluhaSO8ejVOZ",\n "GR7awpEBluhaSO8ejVOZ",\n "Gh7awpEBluhaSO8ejVOZ",\n "Gx7awpEBluhaSO8ejVOZ",\n "HB7awpEBluhaSO8ejVOZ",\n "HR7awpEBluhaSO8ejVOZ",\n "Hh7awpEBluhaSO8ejVOZ",\n "Hx7awpEBluhaSO8ejVOZ",\n "IB7awpEBluhaSO8ejVOZ",\n "IR7awpEBluhaSO8ejVOZ",\n "Ih7awpEBluhaSO8ejVOZ",\n "Ix7awpEBluhaSO8ejVOZ",\n "JB7awpEBluhaSO8ejVOZ",\n "JR7awpEBluhaSO8ejVOZ",\n "Jh7awpEBluhaSO8ejVOZ",\n "Jx7awpEBluhaSO8ejVOZ",\n "KB7awpEBluhaSO8ejVOZ",\n "KR7awpEBluhaSO8ejVOZ",\n "Kh7awpEBluhaSO8ejVOZ",\n "Kx7awpEBluhaSO8ejVOZ",\n "LB7awpEBluhaSO8ejVOZ"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 6, + }, + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 201",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T16:03:46.972Z",\n "gte": "2024-09-05T15:42:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "LB7awpEBluhaSO8ejVOZ",\n "LR7awpEBluhaSO8ejVOZ",\n "Lh7awpEBluhaSO8ejVOZ",\n "Lx7awpEBluhaSO8ejVOZ",\n "MB7awpEBluhaSO8ejVOZ",\n "MR7awpEBluhaSO8ejVOZ",\n "Mh7awpEBluhaSO8ejVOZ",\n "Mx7awpEBluhaSO8ejVOZ",\n "NB7awpEBluhaSO8ejVOZ",\n "NR7awpEBluhaSO8ejVOZ",\n "Nh7awpEBluhaSO8ejVOZ",\n "Nx7awpEBluhaSO8ejVOZ",\n "OB7awpEBluhaSO8ejVOZ",\n "OR7awpEBluhaSO8ejVOZ",\n "Oh7awpEBluhaSO8ejVOZ",\n "Ox7awpEBluhaSO8ejVOZ",\n "PB7awpEBluhaSO8ejVOZ",\n "PR7awpEBluhaSO8ejVOZ",\n "Ph7awpEBluhaSO8ejVOZ",\n "Px7awpEBluhaSO8ejVOZ",\n "QB7awpEBluhaSO8ejVOZ",\n "QR7awpEBluhaSO8ejVOZ",\n "Qh7awpEBluhaSO8ejVOZ",\n "Qx7awpEBluhaSO8ejVOZ",\n "RB7awpEBluhaSO8ejVOZ",\n "RR7awpEBluhaSO8ejVOZ",\n "Rh7awpEBluhaSO8ejVOZ",\n "Rx7awpEBluhaSO8ejVOZ",\n "SB7awpEBluhaSO8ejVOZ",\n "SR7awpEBluhaSO8ejVOZ",\n "Sx7awpEBluhaSO8ewFOg",\n "TB7awpEBluhaSO8ewFOg",\n "TR7awpEBluhaSO8ewFOg",\n "Th7awpEBluhaSO8ewFOg",\n "Tx7awpEBluhaSO8ewFOg",\n "UB7awpEBluhaSO8ewFOg",\n "UR7awpEBluhaSO8ewFOg",\n "Uh7awpEBluhaSO8ewFOh",\n "Ux7awpEBluhaSO8ewFOh",\n "VB7awpEBluhaSO8ewFOh",\n "VR7awpEBluhaSO8ewFOh",\n "Vh7awpEBluhaSO8ewFOh",\n "Vx7awpEBluhaSO8ewFOh",\n "WB7awpEBluhaSO8ewFOh",\n "WR7awpEBluhaSO8ewFOh",\n "Wh7awpEBluhaSO8ewFOh",\n "Wx7awpEBluhaSO8ewFOh",\n "XB7awpEBluhaSO8ewFOh",\n "XR7awpEBluhaSO8ewFOh",\n "Xh7awpEBluhaSO8ewFOh",\n "Xx7awpEBluhaSO8ewFOh",\n "YB7awpEBluhaSO8ewFOh",\n "YR7awpEBluhaSO8ewFOh",\n "Yh7awpEBluhaSO8ewFOh",\n "Yx7awpEBluhaSO8ewFOh",\n "ZB7awpEBluhaSO8ewFOh",\n "ZR7awpEBluhaSO8ewFOh",\n "Zh7awpEBluhaSO8ewFOh",\n "Zx7awpEBluhaSO8ewFOh",\n "aB7awpEBluhaSO8ewFOh",\n "aR7awpEBluhaSO8ewFOh",\n "ah7awpEBluhaSO8ewFOh",\n "ax7awpEBluhaSO8ewFOh",\n "bB7awpEBluhaSO8ewFOh",\n "bR7awpEBluhaSO8ewFOh",\n "bh7awpEBluhaSO8ewFOh",\n "bx7awpEBluhaSO8ewFOh",\n "cB7awpEBluhaSO8ewFOh",\n "cR7awpEBluhaSO8ewFOh",\n "ch7awpEBluhaSO8ewFOh",\n "cx7awpEBluhaSO8ewFOh",\n "dB7awpEBluhaSO8ewFOh",\n "dR7awpEBluhaSO8ewFOh",\n "dh7awpEBluhaSO8ewFOh",\n "dx7awpEBluhaSO8ewFOh",\n "eB7awpEBluhaSO8ewFOh",\n "eR7awpEBluhaSO8ewFOh",\n "eh7awpEBluhaSO8ewFOh",\n "ex7awpEBluhaSO8ewFOh",\n "fB7awpEBluhaSO8ewFOh",\n "fR7awpEBluhaSO8ewFOh",\n "fh7awpEBluhaSO8ewFOh",\n "fx7awpEBluhaSO8ewFOh",\n "gB7awpEBluhaSO8ewFOh",\n "gR7awpEBluhaSO8ewFOh",\n "gh7awpEBluhaSO8ewFOh",\n "gx7awpEBluhaSO8ewFOh",\n "hB7awpEBluhaSO8ewFOh",\n "hR7awpEBluhaSO8ewFOh",\n "hh7awpEBluhaSO8ewFOh",\n "hx7awpEBluhaSO8ewFOh",\n "iB7awpEBluhaSO8ewFOh",\n "iR7awpEBluhaSO8ewFOh",\n "ih7awpEBluhaSO8ewFOh",\n "ix7awpEBluhaSO8ewFOh",\n "jB7awpEBluhaSO8ewFOh",\n "jR7awpEBluhaSO8ewFOh",\n "jh7awpEBluhaSO8ewFOh",\n "jx7awpEBluhaSO8ewFOh",\n "kB7awpEBluhaSO8ewFOh",\n "kR7awpEBluhaSO8ewFOh"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 8, + }, + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 301",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T16:03:46.972Z",\n "gte": "2024-09-05T15:42:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "kR7awpEBluhaSO8ewFOh",\n "kh7awpEBluhaSO8ewFOh",\n "kx7awpEBluhaSO8ewFOh",\n "lB7awpEBluhaSO8ewFOh",\n "lR7awpEBluhaSO8ewFOh",\n "lh7awpEBluhaSO8ewFOh",\n "lx7awpEBluhaSO8ewFOh",\n "mB7awpEBluhaSO8ewFOh",\n "mR7awpEBluhaSO8ewFOh",\n "mh7awpEBluhaSO8ewFOh",\n "mx7awpEBluhaSO8ewFOh",\n "nB7awpEBluhaSO8ewFOh",\n "nR7awpEBluhaSO8ewFOh",\n "nh7awpEBluhaSO8ewFOh",\n "nx7awpEBluhaSO8ewFOh",\n "oB7awpEBluhaSO8ewFOh",\n "oR7awpEBluhaSO8ewFOh",\n "oh7awpEBluhaSO8ewFOh",\n "ox7awpEBluhaSO8ewFOh",\n "pB7awpEBluhaSO8ewFOh",\n "pR7awpEBluhaSO8ewFOh",\n "ph7awpEBluhaSO8ewFOh",\n "px7awpEBluhaSO8ewFOh",\n "qB7awpEBluhaSO8ewFOh",\n "qR7awpEBluhaSO8ewFOh",\n "qh7awpEBluhaSO8ewFOh",\n "qx7awpEBluhaSO8ewFOh",\n "rB7awpEBluhaSO8ewFOh",\n "rR7awpEBluhaSO8ewFOh",\n "rh7awpEBluhaSO8ewFOh",\n "rx7awpEBluhaSO8ewFOh",\n "sB7awpEBluhaSO8ewFOh",\n "sR7awpEBluhaSO8ewFOh",\n "sh7awpEBluhaSO8ewFOh",\n "sx7awpEBluhaSO8ewFOh",\n "tB7awpEBluhaSO8ewFOh",\n "tR7awpEBluhaSO8ewFOh",\n "th7awpEBluhaSO8ewFOh",\n "tx7awpEBluhaSO8ewFOh",\n "uB7awpEBluhaSO8ewFOh",\n "uR7awpEBluhaSO8ewFOh",\n "uh7awpEBluhaSO8ewFOh",\n "ux7awpEBluhaSO8ewFOh",\n "vB7awpEBluhaSO8ewFOh",\n "vR7awpEBluhaSO8ewFOh",\n "vh7awpEBluhaSO8ewFOh",\n "vx7awpEBluhaSO8ewFOh",\n "wB7awpEBluhaSO8ewFOh",\n "wR7awpEBluhaSO8ewFOh",\n "wh7awpEBluhaSO8ewFOh",\n "wx7awpEBluhaSO8ewFOh",\n "xB7awpEBluhaSO8ewFOh",\n "xR7awpEBluhaSO8ewFOh",\n "xh7awpEBluhaSO8ewFOh",\n "xx7awpEBluhaSO8ewFOh",\n "yB7awpEBluhaSO8ewFOh",\n "yR7awpEBluhaSO8ewFOh",\n "yh7awpEBluhaSO8ewFOh",\n "yx7awpEBluhaSO8ewFOh",\n "zB7awpEBluhaSO8ewFOh",\n "zR7awpEBluhaSO8ewFOh",\n "zh7awpEBluhaSO8ewFOh",\n "zx7awpEBluhaSO8ewFOh",\n "0B7awpEBluhaSO8ewFOh",\n "0R7awpEBluhaSO8ewFOh",\n "0h7awpEBluhaSO8ewFOh",\n "0x7awpEBluhaSO8ewFOh",\n "1B7awpEBluhaSO8ewFOh",\n "1R7awpEBluhaSO8ewFOh",\n "1h7awpEBluhaSO8ewFOh",\n "1x7awpEBluhaSO8ewFOh",\n "2B7awpEBluhaSO8ewFOh",\n "2R7awpEBluhaSO8ewFOh",\n "2h7awpEBluhaSO8ewFOh",\n "2x7awpEBluhaSO8ewFOh",\n "3B7awpEBluhaSO8ewFOh",\n "3R7awpEBluhaSO8ewFOh",\n "3h7awpEBluhaSO8ewFOh",\n "3x7awpEBluhaSO8ewFOh",\n "4B7awpEBluhaSO8ewFOh",\n "4R7awpEBluhaSO8ewFOh",\n "4h7awpEBluhaSO8ewFOh",\n "4x7awpEBluhaSO8ewFOh",\n "5B7awpEBluhaSO8ewFOh",\n "5R7awpEBluhaSO8ewFOh",\n "5h7awpEBluhaSO8ewFOh",\n "6h7awpEBluhaSO8e51Pb",\n "6x7awpEBluhaSO8e51Pb",\n "7B7awpEBluhaSO8e51Pb",\n "7R7awpEBluhaSO8e51Pb",\n "7h7awpEBluhaSO8e51Pb",\n "7x7awpEBluhaSO8e51Pb",\n "8B7awpEBluhaSO8e51Pb",\n "8R7awpEBluhaSO8e51Pb",\n "8h7awpEBluhaSO8e51Pb",\n "8x7awpEBluhaSO8e51Pb",\n "9B7awpEBluhaSO8e51Pb",\n "9R7awpEBluhaSO8e51Pb",\n "9h7awpEBluhaSO8e51Pb",\n "9x7awpEBluhaSO8e51Pb",\n "-B7awpEBluhaSO8e51Pb"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 7, + }, + ], + }, + { + errors: [], + warnings: [], + startedAt: '2024-09-05T16:23:46.972Z', + duration: 103, + requests: [ + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 101",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T16:23:46.972Z",\n "gte": "2024-09-05T16:02:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + duration: 19, + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "_B7_wpEBluhaSO8enqFT",\n "_R7_wpEBluhaSO8enqFT",\n "_h7_wpEBluhaSO8enqFT",\n "_x7_wpEBluhaSO8enqFT",\n "AB7_wpEBluhaSO8enqJT",\n "AR7_wpEBluhaSO8enqJT",\n "Ah7_wpEBluhaSO8enqJT",\n "Ax7_wpEBluhaSO8enqJT",\n "BB7_wpEBluhaSO8enqJT",\n "BR7_wpEBluhaSO8enqJT",\n "Bh7_wpEBluhaSO8enqJT",\n "Bx7_wpEBluhaSO8enqJT",\n "CB7_wpEBluhaSO8enqJT",\n "CR7_wpEBluhaSO8enqJT",\n "Ch7_wpEBluhaSO8enqJT",\n "Cx7_wpEBluhaSO8enqJT",\n "DB7_wpEBluhaSO8enqJT",\n "DR7_wpEBluhaSO8enqJT",\n "Dh7_wpEBluhaSO8enqJT",\n "Dx7_wpEBluhaSO8enqJT",\n "EB7_wpEBluhaSO8enqJT",\n "ER7_wpEBluhaSO8enqJT",\n "Eh7_wpEBluhaSO8enqJT",\n "Ex7_wpEBluhaSO8enqJT",\n "FB7_wpEBluhaSO8enqJT",\n "FR7_wpEBluhaSO8enqJT",\n "Fh7_wpEBluhaSO8enqJT",\n "Fx7_wpEBluhaSO8enqJT",\n "GB7_wpEBluhaSO8enqJT",\n "GR7_wpEBluhaSO8enqJT",\n "Gh7_wpEBluhaSO8enqJT",\n "Gx7_wpEBluhaSO8enqJT",\n "tR7wwpEBluhaSO8efnLO",\n "th7wwpEBluhaSO8efnLO",\n "tx7wwpEBluhaSO8efnLO",\n "uB7wwpEBluhaSO8efnLO",\n "uR7wwpEBluhaSO8efnLO",\n "uh7wwpEBluhaSO8efnLO",\n "ux7wwpEBluhaSO8efnLO",\n "vB7wwpEBluhaSO8efnLO",\n "vR7wwpEBluhaSO8efnLO",\n "vh7wwpEBluhaSO8efnLO",\n "vx7wwpEBluhaSO8efnLO",\n "wB7wwpEBluhaSO8efnLO",\n "wR7wwpEBluhaSO8efnLO",\n "wh7wwpEBluhaSO8efnLO",\n "wx7wwpEBluhaSO8efnLO",\n "xB7wwpEBluhaSO8efnLO",\n "xR7wwpEBluhaSO8efnLO",\n "xh7wwpEBluhaSO8efnLO",\n "xx7wwpEBluhaSO8efnLO",\n "yB7wwpEBluhaSO8efnLO",\n "yR7wwpEBluhaSO8efnLO",\n "yh7wwpEBluhaSO8efnLO",\n "yx7wwpEBluhaSO8efnLO",\n "zB7wwpEBluhaSO8efnLO",\n "zR7wwpEBluhaSO8efnLO",\n "zh7wwpEBluhaSO8efnLO",\n "zx7wwpEBluhaSO8efnLO",\n "0B7wwpEBluhaSO8efnLO",\n "0R7wwpEBluhaSO8efnLO",\n "0h7wwpEBluhaSO8efnLO",\n "0x7wwpEBluhaSO8efnLO",\n "1B7wwpEBluhaSO8efnLO",\n "1B7twpEBluhaSO8eu1-P",\n "1R7twpEBluhaSO8eu1-P",\n "1h7twpEBluhaSO8eu1-P",\n "1x7twpEBluhaSO8eu1-P",\n "2B7twpEBluhaSO8eu1-P",\n "2R7twpEBluhaSO8eu1-P",\n "2h7twpEBluhaSO8eu1-P",\n "2x7twpEBluhaSO8eu1-P",\n "3B7twpEBluhaSO8eu1-P",\n "3R7twpEBluhaSO8eu1-P",\n "3h7twpEBluhaSO8eu1-P",\n "3x7twpEBluhaSO8eu1-P",\n "4B7twpEBluhaSO8eu1-P",\n "4R7twpEBluhaSO8eu1-P",\n "4h7twpEBluhaSO8eu1-P",\n "4x7twpEBluhaSO8eu1-P",\n "5B7twpEBluhaSO8eu1-P",\n "5R7twpEBluhaSO8eu1-P",\n "5h7twpEBluhaSO8eu1-P",\n "5x7twpEBluhaSO8eu1-P",\n "6B7twpEBluhaSO8eu1-P",\n "6R7twpEBluhaSO8eu1-P",\n "6h7twpEBluhaSO8eu1-P",\n "6x7twpEBluhaSO8eu1-P",\n "7B7twpEBluhaSO8eu1-P",\n "7R7twpEBluhaSO8eu1-P",\n "7h7twpEBluhaSO8eu1-P",\n "7x7twpEBluhaSO8eu1-P",\n "8B7twpEBluhaSO8eu1-P",\n "8R7twpEBluhaSO8eu1-P",\n "8h7twpEBluhaSO8eu1-P",\n "8x7twpEBluhaSO8eu1-P",\n "HB7_wpEBluhaSO8enqJT",\n "HR7_wpEBluhaSO8enqJT",\n "Hh7_wpEBluhaSO8enqJT",\n "Hx7_wpEBluhaSO8enqJT",\n "IB7_wpEBluhaSO8enqJT"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 5, + }, + ], + }, +]; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx index 69eebec3452d5..4ebb460177476 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx @@ -6,10 +6,11 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import type { DataViewBase } from '@kbn/es-query'; import { fields } from '@kbn/data-plugin/common/mocks'; +import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { TestProviders } from '../../../../common/mock'; import type { RulePreviewProps } from '.'; @@ -22,6 +23,7 @@ import { stepDefineDefaultValue, } from '../../../../detections/pages/detection_engine/rules/utils'; import { usePreviewInvocationCount } from './use_preview_invocation_count'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; jest.mock('../../../../common/lib/kibana'); jest.mock('./use_preview_route'); @@ -34,6 +36,21 @@ jest.mock('../../../../common/containers/use_global_time', () => ({ }), })); jest.mock('./use_preview_invocation_count'); +jest.mock('../../../../common/hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: jest.fn(), +})); + +const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; +// rule types that do not support logged requests +const doNotSupportLoggedRequests: Type[] = [ + 'threshold', + 'threat_match', + 'machine_learning', + 'query', + 'new_terms', +]; + +const supportLoggedRequests: Type[] = ['esql', 'eql']; const getMockIndexPattern = (): DataViewBase => ({ fields, @@ -97,6 +114,8 @@ describe('PreviewQuery', () => { }); (usePreviewInvocationCount as jest.Mock).mockReturnValue({ invocationCount: 500 }); + + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); }); afterEach(() => { @@ -137,4 +156,51 @@ describe('PreviewQuery', () => { expect(await wrapper.findByTestId('previewInvocationCountWarning')).toBeTruthy(); }); + + supportLoggedRequests.forEach((ruleType) => { + test(`renders "Show Elasticsearch requests" for ${ruleType} rule type`, () => { + render( + + + + ); + + expect(screen.getByTestId('show-elasticsearch-requests')).toBeInTheDocument(); + }); + }); + + supportLoggedRequests.forEach((ruleType) => { + test(`does not render "Show Elasticsearch requests" for ${ruleType} rule type when feature is disabled`, () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(false); + + render( + + + + ); + + expect(screen.queryByTestId('show-elasticsearch-requests')).toBeNull(); + }); + }); + + doNotSupportLoggedRequests.forEach((ruleType) => { + test(`does not render "Show Elasticsearch requests" for ${ruleType} rule type`, () => { + render( + + + + ); + + expect(screen.queryByTestId('show-elasticsearch-requests')).toBeNull(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx index 8deb2ebd41863..2a86600d94e7a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx @@ -18,9 +18,12 @@ import { EuiText, EuiTitle, EuiFormRow, + EuiCheckbox, } from '@elastic/eui'; import moment from 'moment'; import type { List } from '@kbn/securitysolution-io-ts-list-types'; +import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; + import { isEqual } from 'lodash'; import * as i18n from './translations'; import { usePreviewRoute } from './use_preview_route'; @@ -37,9 +40,12 @@ import type { TimeframePreviewOptions, } from '../../../../detections/pages/detection_engine/rules/types'; import { usePreviewInvocationCount } from './use_preview_invocation_count'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; export const REASONABLE_INVOCATION_COUNT = 200; +const RULE_TYPES_SUPPORTING_LOGGED_REQUESTS: Type[] = ['esql', 'eql']; + const timeRanges = [ { start: 'now/d', end: 'now', label: 'Today' }, { start: 'now/w', end: 'now', label: 'This week' }, @@ -64,6 +70,7 @@ interface RulePreviewState { aboutRuleData?: AboutStepRule; scheduleRuleData?: ScheduleStepRule; timeframeOptions: TimeframePreviewOptions; + enableLoggedRequests?: boolean; } const refreshedTimeframe = (startDate: string, endDate: string) => { @@ -83,6 +90,8 @@ const RulePreviewComponent: React.FC = ({ const { indexPattern, ruleType } = defineRuleData; const { spaces } = useKibana().services; + const isLoggingRequestsFeatureEnabled = useIsExperimentalFeatureEnabled('loggingRequestsEnabled'); + const [spaceId, setSpaceId] = useState(''); useEffect(() => { if (spaces) { @@ -98,6 +107,8 @@ const RulePreviewComponent: React.FC = ({ const [timeframeStart, setTimeframeStart] = useState(moment().subtract(1, 'hour')); const [timeframeEnd, setTimeframeEnd] = useState(moment()); + const [showElasticsearchRequests, setShowElasticsearchRequests] = useState(false); + const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); useEffect(() => { @@ -140,6 +151,7 @@ const RulePreviewComponent: React.FC = ({ scheduleRuleData: previewData.scheduleRuleData, exceptionsList, timeframeOptions: previewData.timeframeOptions, + enableLoggedRequests: previewData.enableLoggedRequests, }); const { startTransaction } = useStartTransaction(); @@ -185,9 +197,18 @@ const RulePreviewComponent: React.FC = ({ interval: scheduleRuleData.interval, lookback: scheduleRuleData.from, }, + enableLoggedRequests: showElasticsearchRequests, }); setIsRefreshing(true); - }, [aboutRuleData, defineRuleData, endDate, scheduleRuleData, startDate, startTransaction]); + }, [ + aboutRuleData, + defineRuleData, + endDate, + scheduleRuleData, + startDate, + startTransaction, + showElasticsearchRequests, + ]); const isDirty = useMemo( () => @@ -261,6 +282,24 @@ const RulePreviewComponent: React.FC = ({ + {isLoggingRequestsFeatureEnabled && + RULE_TYPES_SUPPORTING_LOGGED_REQUESTS.includes(ruleType) ? ( + + + + { + setShowElasticsearchRequests(!showElasticsearchRequests); + }} + /> + + + + ) : null} {isPreviewRequestInProgress && } {!isPreviewRequestInProgress && previewId && spaceId && ( @@ -273,7 +312,12 @@ const RulePreviewComponent: React.FC = ({ timeframeOptions={previewData.timeframeOptions} /> )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.test.tsx new file mode 100644 index 0000000000000..c0cf8870c162a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.test.tsx @@ -0,0 +1,68 @@ +/* + * 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 React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { TestProviders } from '../../../../common/mock/test_providers'; +import { LoggedRequests } from './logged_requests'; + +import { previewLogs } from './__mocks__/preview_logs'; + +describe('LoggedRequests', () => { + it('should not render component if logs are empty', () => { + render(, { wrapper: TestProviders }); + + expect(screen.queryByTestId('preview-logged-requests-accordion')).toBeNull(); + }); + + it('should open accordion on click and render list of request items', async () => { + render(, { wrapper: TestProviders }); + + expect(screen.queryByTestId('preview-logged-requests-accordion')).toBeInTheDocument(); + + await userEvent.click(screen.getByText('Preview logged requests')); + + expect(screen.getAllByTestId('preview-logged-requests-item-accordion')).toHaveLength(3); + }); + + it('should render code content on logged request item accordion click', async () => { + render(, { wrapper: TestProviders }); + + expect(screen.queryByTestId('preview-logged-requests-accordion')).toBeInTheDocument(); + + await userEvent.click(screen.getByText('Preview logged requests')); + + // picking up second rule execution + const loggedRequestsItem = screen.getAllByTestId('preview-logged-requests-item-accordion')[1]; + + expect(loggedRequestsItem).toHaveTextContent('Rule execution started at'); + expect(loggedRequestsItem).toHaveTextContent('[269ms]'); + + await userEvent.click(loggedRequestsItem.querySelector('button') as HTMLElement); + + expect(screen.getAllByTestId('preview-logged-request-description')).toHaveLength(6); + expect(screen.getAllByTestId('preview-logged-request-code-block')).toHaveLength(6); + + expect(screen.getAllByTestId('preview-logged-request-description')[0]).toHaveTextContent( + 'ES|QL request to find all matches [30ms]' + ); + + expect(screen.getAllByTestId('preview-logged-request-code-block')[0]).toHaveTextContent( + /FROM packetbeat-8\.14\.2 metadata _id, _version, _index \| limit 101/ + ); + + expect(screen.getAllByTestId('preview-logged-request-description')[1]).toHaveTextContent( + 'Retrieve source documents when ES|QL query is not aggregable' + ); + + expect(screen.getAllByTestId('preview-logged-request-code-block')[1]).toHaveTextContent( + /POST \/packetbeat-8\.14\.2\/_search\?ignore_unavailable=true/ + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.tsx new file mode 100644 index 0000000000000..d7b62c6f08c69 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.tsx @@ -0,0 +1,58 @@ +/* + * 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 { FC } from 'react'; +import React, { useMemo } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { css } from '@emotion/css'; + +import type { RulePreviewLogs } from '../../../../../common/api/detection_engine'; +import * as i18n from './translations'; +import { OptimizedAccordion } from './optimized_accordion'; +import { LoggedRequestsItem } from './logged_requests_item'; +import { useAccordionStyling } from './use_accordion_styling'; + +const LoggedRequestsComponent: FC<{ logs: RulePreviewLogs[] }> = ({ logs }) => { + const cssStyles = useAccordionStyling(); + + const AccordionContent = useMemo( + () => ( + <> + + {logs.map((log) => ( + + + + ))} + + ), + [logs] + ); + + if (logs.length === 0) { + return null; + } + + return ( + <> + + {AccordionContent} + + + ); +}; + +export const LoggedRequests = React.memo(LoggedRequestsComponent); +LoggedRequests.displayName = 'LoggedRequests'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests_item.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests_item.tsx new file mode 100644 index 0000000000000..2f2e7d74bf826 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests_item.tsx @@ -0,0 +1,79 @@ +/* + * 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 { FC, PropsWithChildren } from 'react'; +import React from 'react'; +import { css } from '@emotion/css'; + +import { EuiSpacer, EuiCodeBlock, useEuiPaddingSize, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { RulePreviewLogs } from '../../../../../common/api/detection_engine'; +import * as i18n from './translations'; +import { PreferenceFormattedDate } from '../../../../common/components/formatted_date'; +import { OptimizedAccordion } from './optimized_accordion'; +import { useAccordionStyling } from './use_accordion_styling'; + +const LoggedRequestsItemComponent: FC> = ({ + startedAt, + duration, + requests, +}) => { + const paddingLarge = useEuiPaddingSize('l'); + const cssStyles = useAccordionStyling(); + + return ( + + {startedAt ? ( + }} + /> + ) : ( + i18n.LOGGED_REQUEST_ITEM_ACCORDION_UNKNOWN_TIME_BUTTON + )} + {`[${duration}ms]`} + + } + id={`ruleExecution-${startedAt}`} + css={css` + margin-left: ${paddingLarge}; + ${cssStyles} + `} + > + {(requests ?? []).map((request, key) => ( + + + + {request?.description ?? null} {request?.duration ? `[${request.duration}ms]` : null} + + + + {request.request} + + + ))} + + ); +}; + +export const LoggedRequestsItem = React.memo(LoggedRequestsItemComponent); +LoggedRequestsItem.displayName = 'LoggedRequestsItem'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.test.tsx new file mode 100644 index 0000000000000..bab5751aa6ddf --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.test.tsx @@ -0,0 +1,59 @@ +/* + * 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 React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { OptimizedAccordion } from './optimized_accordion'; + +describe('OptimizedAccordion', () => { + it('should not render children content if accordion initially closed', () => { + render( + + {'content'} + + ); + + expect(screen.queryByText('content')).toBeNull(); + }); + it('should render children content if accordion initially opened', () => { + render( + + {'content'} + + ); + + expect(screen.getByText('content')).toBeInTheDocument(); + }); + it('should render children content when accordion opened', async () => { + render( + + {'content'} + + ); + + const toggleButton = screen.getByText('accordion button'); + await userEvent.click(toggleButton); + + expect(screen.getByText('content')).toBeVisible(); + }); + it('should not destroy children content when accordion closed', async () => { + render( + + {'content'} + + ); + + const toggleButton = screen.getByText('accordion button'); + await userEvent.click(toggleButton); + + expect(screen.getByText('content')).toBeVisible(); + + await userEvent.click(toggleButton); + expect(screen.getByText('content')).not.toBeVisible(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.tsx new file mode 100644 index 0000000000000..5d3ad0ab874be --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.tsx @@ -0,0 +1,39 @@ +/* + * 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 { FC } from 'react'; + +import React, { useState } from 'react'; + +import type { EuiAccordionProps } from '@elastic/eui'; +import { EuiAccordion } from '@elastic/eui'; + +/** + * component does not render children before it was opened + * once children rendered for the first time, they won't be re-rendered on subsequent accordion toggling + */ +const OptimizedAccordionComponent: FC = ({ children, ...props }) => { + const [trigger, setTrigger] = useState<'closed' | 'open'>('closed'); + const [isRendered, setIsRendered] = useState(false); + + const onToggle = (isOpen: boolean) => { + const newState = isOpen ? 'open' : 'closed'; + if (isOpen) { + setIsRendered(true); + } + setTrigger(newState); + }; + + return ( + + {isRendered || props.forceState === 'open' ? children : null} + + ); +}; + +export const OptimizedAccordion = React.memo(OptimizedAccordionComponent); +OptimizedAccordion.displayName = 'OptimizedAccordion'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_logs.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_logs.tsx index 912df2453cc46..196408cbc1371 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_logs.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_logs.tsx @@ -7,14 +7,19 @@ import type { FC, PropsWithChildren } from 'react'; import React, { Fragment, useMemo } from 'react'; +import { css } from '@emotion/css'; import { EuiCallOut, EuiText, EuiSpacer, EuiAccordion } from '@elastic/eui'; + import type { RulePreviewLogs } from '../../../../../common/api/detection_engine'; import * as i18n from './translations'; +import { LoggedRequests } from './logged_requests'; +import { useAccordionStyling } from './use_accordion_styling'; interface PreviewLogsProps { logs: RulePreviewLogs[]; hasNoiseWarning: boolean; isAborted: boolean; + showElasticsearchRequests: boolean; } interface SortedLogs { @@ -43,7 +48,12 @@ const addLogs = ( allLogs: SortedLogs[] ) => (logs.length ? [{ startedAt, logs, duration }, ...allLogs] : allLogs); -const PreviewLogsComponent: React.FC = ({ logs, hasNoiseWarning, isAborted }) => { +const PreviewLogsComponent: React.FC = ({ + logs, + hasNoiseWarning, + isAborted, + showElasticsearchRequests, +}) => { const sortedLogs = useMemo( () => logs.reduce<{ @@ -66,6 +76,7 @@ const PreviewLogsComponent: React.FC = ({ logs, hasNoiseWarnin {isAborted ? : null} + {showElasticsearchRequests ? : null} ); }; @@ -74,6 +85,8 @@ export const PreviewLogs = React.memo(PreviewLogsComponent); PreviewLogs.displayName = 'PreviewLogs'; const LogAccordion: FC> = ({ logs, isError, children }) => { + const cssStyles = useAccordionStyling(); + const firstLog = logs[0]; if (!(children || firstLog)) return null; @@ -96,6 +109,10 @@ const LogAccordion: FC> = ({ logs, isError, buttonContent={ isError ? i18n.QUERY_PREVIEW_SEE_ALL_ERRORS : i18n.QUERY_PREVIEW_SEE_ALL_WARNINGS } + borders="horizontal" + css={css` + ${cssStyles} + `} > {restOfLogs.map((log, key) => ( > = ({ logs, isError, ))} ) : null} - ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/translations.ts index 4ab9328ee806b..0b25071a76830 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/translations.ts @@ -158,6 +158,27 @@ export const VIEW_DETAILS = i18n.translate( } ); +export const ENABLED_LOGGED_REQUESTS_CHECKBOX = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.enabledLoggedRequestsLabel', + { + defaultMessage: 'Show Elasticsearch requests, ran during rule executions', + } +); + +export const LOGGED_REQUESTS_ACCORDION_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.loggedRequestsAccordionButtonLabel', + { + defaultMessage: 'Preview logged requests', + } +); + +export const LOGGED_REQUEST_ITEM_ACCORDION_UNKNOWN_TIME_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.loggedRequestItemAccordionUnknownTimeButtonLabel', + { + defaultMessage: 'Preview logged requests', + } +); + export const VIEW_DETAILS_FOR_ROW = ({ ariaRowindex, columnValues, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_accordion_styling.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_accordion_styling.ts new file mode 100644 index 0000000000000..be05d90836c94 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_accordion_styling.ts @@ -0,0 +1,16 @@ +/* + * 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 { useEuiPaddingSize } from '@elastic/eui'; + +export const useAccordionStyling = () => { + const paddingLarge = useEuiPaddingSize('l'); + const paddingSmall = useEuiPaddingSize('s'); + + return `padding-bottom: ${paddingLarge}; + padding-top: ${paddingSmall};`; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_route.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_route.tsx index 25952956b2538..5684819106ada 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_route.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_route.tsx @@ -24,6 +24,7 @@ interface PreviewRouteParams { scheduleRuleData?: ScheduleStepRule; exceptionsList?: List[]; timeframeOptions: TimeframePreviewOptions; + enableLoggedRequests?: boolean; } export const usePreviewRoute = ({ @@ -32,6 +33,7 @@ export const usePreviewRoute = ({ scheduleRuleData, exceptionsList, timeframeOptions, + enableLoggedRequests, }: PreviewRouteParams) => { const [isRequestTriggered, setIsRequestTriggered] = useState(false); @@ -41,6 +43,7 @@ export const usePreviewRoute = ({ const { isLoading, response, rule, setRule } = usePreviewRule({ timeframeOptions, + enableLoggedRequests, }); const [logs, setLogs] = useState(response.logs ?? []); const [isAborted, setIsAborted] = useState(!!response.isAborted); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts index 9fb7417bca036..05c3b9fe10299 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts @@ -27,8 +27,10 @@ const emptyPreviewRule: RulePreviewResponse = { export const usePreviewRule = ({ timeframeOptions, + enableLoggedRequests, }: { timeframeOptions: TimeframePreviewOptions; + enableLoggedRequests?: boolean; }) => { const [rule, setRule] = useState(null); const [response, setResponse] = useState(emptyPreviewRule); @@ -66,6 +68,7 @@ export const usePreviewRule = ({ invocationCount, timeframeEnd, }, + enableLoggedRequests, signal: abortCtrl.signal, }); if (isSubscribed) { @@ -87,7 +90,7 @@ export const usePreviewRule = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [rule, addError, invocationCount, from, interval, timeframeEnd]); + }, [rule, addError, invocationCount, from, interval, timeframeEnd, enableLoggedRequests]); return { isLoading, response, rule, setRule }; }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts index 5180f9fb891f0..d10bb4bb03e08 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts @@ -117,6 +117,21 @@ describe('Detections Rules API', () => { expect.objectContaining({ body: '{"description":"Detecting root and admin users","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","rule_id":"rule-1","invocationCount":1,"timeframeEnd":"2015-03-12 05:17:10"}', method: 'POST', + query: undefined, + }) + ); + }); + + test('sends enable_logged_requests in URL query', async () => { + const payload = getCreateRulesSchemaMock(); + await previewRule({ + rule: { ...payload, invocationCount: 1, timeframeEnd: '2015-03-12 05:17:10' }, + enableLoggedRequests: true, + }); + expect(fetchMock).toHaveBeenCalledWith( + '/api/detection_engine/rules/preview', + expect.objectContaining({ + query: { enable_logged_requests: true }, }) ); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index 4254b58234400..c86606d0d8137 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -150,6 +150,7 @@ export const patchRule = async ({ */ export const previewRule = async ({ rule, + enableLoggedRequests, signal, }: PreviewRulesProps): Promise => KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_PREVIEW, { @@ -157,6 +158,7 @@ export const previewRule = async ({ version: '2023-10-31', body: JSON.stringify(rule), signal, + query: enableLoggedRequests ? { enable_logged_requests: enableLoggedRequests } : undefined, }); /** diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts index 526d0a00389d7..e12442c97aa4c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts @@ -32,7 +32,11 @@ export interface CreateRulesProps { } export interface PreviewRulesProps { - rule: RuleCreateProps & { invocationCount: number; timeframeEnd: string }; + rule: RuleCreateProps & { + invocationCount: number; + timeframeEnd: string; + }; + enableLoggedRequests?: boolean; signal?: AbortSignal; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts index 52f4d3739b1e2..50542592aa1d1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts @@ -31,7 +31,11 @@ import type { RulePreviewResponse, RulePreviewLogs, } from '../../../../../../common/api/detection_engine'; -import { RulePreviewRequestBody } from '../../../../../../common/api/detection_engine'; +import { + RulePreviewRequestBody, + RulePreviewRequestQuery, +} from '../../../../../../common/api/detection_engine'; +import type { RulePreviewLoggedRequest } from '../../../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; import type { StartPlugins, SetupPlugins } from '../../../../../plugin'; import { buildSiemResponse } from '../../../routes/utils'; @@ -92,7 +96,12 @@ export const previewRulesRoute = ( .addVersion( { version: '2023-10-31', - validate: { request: { body: buildRouteValidationWithZod(RulePreviewRequestBody) } }, + validate: { + request: { + body: buildRouteValidationWithZod(RulePreviewRequestBody), + query: buildRouteValidationWithZod(RulePreviewRequestQuery), + }, + }, }, async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); @@ -143,7 +152,9 @@ export const previewRulesRoute = ( const username = security?.authc.getCurrentUser(request)?.username; const loggedStatusChanges: Array = []; const previewRuleExecutionLogger = createPreviewRuleExecutionLogger(loggedStatusChanges); - const runState: Record = {}; + const runState: Record = { + isLoggedRequestsEnabled: request.query.enable_logged_requests, + }; const logs: RulePreviewLogs[] = []; let isAborted = false; @@ -224,6 +235,7 @@ export const previewRulesRoute = ( } ) => { let statePreview = runState as TState; + let loggedRequests = []; const abortController = new AbortController(); setTimeout(() => { @@ -268,7 +280,7 @@ export const previewRulesRoute = ( while (invocationCount > 0 && !isAborted) { invocationStartTime = moment(); - ({ state: statePreview } = (await executor({ + ({ state: statePreview, loggedRequests } = (await executor({ executionId: uuidv4(), params, previousStartedAt, @@ -302,7 +314,7 @@ export const previewRulesRoute = ( const date = startedAt.toISOString(); return { dateStart: date, dateEnd: date }; }, - })) as { state: TState }); + })) as { state: TState; loggedRequests: RulePreviewLoggedRequest[] }); const errors = loggedStatusChanges .filter((item) => item.newStatus === RuleExecutionStatusEnum.failed) @@ -317,6 +329,7 @@ export const previewRulesRoute = ( warnings, startedAt: startedAt.toDate().toISOString(), duration: moment().diff(invocationStartTime, 'milliseconds'), + ...(loggedRequests ? { requests: loggedRequests } : {}), }); loggedStatusChanges.length = 0; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index 1e5e70a37ae5f..f25a8429089b6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -467,6 +467,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = warning: warningMessages.length > 0, warningMessages, userError: runResult.userError, + ...(runResult.loggedRequests ? { loggedRequests: runResult.loggedRequests } : {}), }; runState = runResult.state; } @@ -571,7 +572,10 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = }); } - return { state: result.state }; + return { + state: result.state, + ...(result.loggedRequests ? { loggedRequests: result.loggedRequests } : {}), + }; }); }, alerts: { 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 ca16b38404e48..9de8641d7b17c 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 @@ -111,7 +111,7 @@ export const createEqlAlertType = ( alertSuppression: completeRule.ruleParams.alertSuppression, licensing, }); - const result = await eqlExecutor({ + const { result, loggedRequests } = await eqlExecutor({ completeRule, tuple, inputIndex, @@ -131,9 +131,10 @@ export const createEqlAlertType = ( alertWithSuppression, isAlertSuppressionActive: isNonSeqAlertSuppressionActive, experimentalFeatures, + state, scheduleNotificationResponseActionsService, }); - return { ...result, state }; + return { ...result, state, ...(loggedRequests ? { loggedRequests } : {}) }; }, }; }; 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 9ef9faeb9de3a..4f5aa7d322c9e 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 @@ -54,7 +54,7 @@ describe('eql_executor', () => { describe('eqlExecutor', () => { describe('warning scenarios', () => { it('warns when exception list for eql rule contains value list exceptions', async () => { - const result = await eqlExecutor({ + const { result } = await eqlExecutor({ inputIndex: DEFAULT_INDEX_PATTERN, runtimeMappings: {}, completeRule: eqlCompleteRule, @@ -105,7 +105,7 @@ describe('eql_executor', () => { }, }); - const result = await eqlExecutor({ + const { result } = await eqlExecutor({ inputIndex: DEFAULT_INDEX_PATTERN, runtimeMappings: {}, completeRule: ruleWithSequenceAndSuppression, @@ -140,7 +140,7 @@ describe('eql_executor', () => { message: 'verification_exception\n\tRoot causes:\n\t\tverification_exception: Found 1 problem\nline 1:1: Unknown column [event.category]', }); - const result = await eqlExecutor({ + const { result } = await eqlExecutor({ inputIndex: DEFAULT_INDEX_PATTERN, runtimeMappings: {}, completeRule: eqlCompleteRule, @@ -165,7 +165,7 @@ describe('eql_executor', () => { }); it('should handle scheduleNotificationResponseActionsService call', async () => { - const result = await eqlExecutor({ + const { result } = await eqlExecutor({ inputIndex: DEFAULT_INDEX_PATTERN, runtimeMappings: {}, completeRule: eqlCompleteRule, 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 3379d0a0c6867..47e298392d7d9 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 @@ -46,6 +46,9 @@ import type { import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring'; import { bulkCreateSuppressedAlertsInMemory } from '../utils/bulk_create_suppressed_alerts_in_memory'; import { getDataTierFilter } from '../utils/get_data_tier_filter'; +import type { RulePreviewLoggedRequest } from '../../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; +import { logEqlRequest } from '../utils/logged_requests'; +import * as i18n from '../translations'; interface EqlExecutorParams { inputIndex: string[]; @@ -67,6 +70,7 @@ interface EqlExecutorParams { alertWithSuppression: SuppressedAlertService; isAlertSuppressionActive: boolean; experimentalFeatures: ExperimentalFeatures; + state?: Record; scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService']; } @@ -90,10 +94,17 @@ export const eqlExecutor = async ({ alertWithSuppression, isAlertSuppressionActive, experimentalFeatures, + state, scheduleNotificationResponseActionsService, -}: EqlExecutorParams): Promise => { +}: EqlExecutorParams): Promise<{ + result: SearchAfterAndBulkCreateReturnType; + loggedRequests?: RulePreviewLoggedRequest[]; +}> => { const ruleParams = completeRule.ruleParams; + const isLoggedRequestsEnabled = state?.isLoggedRequestsEnabled ?? false; + const loggedRequests: RulePreviewLoggedRequest[] = []; + // eslint-disable-next-line complexity return withSecuritySpan('eqlExecutor', async () => { const result = createSearchAfterReturnType(); @@ -125,13 +136,24 @@ export const eqlExecutor = async ({ const eqlSignalSearchStart = performance.now(); try { + if (isLoggedRequestsEnabled) { + loggedRequests.push({ + request: logEqlRequest(request), + description: i18n.EQL_SEARCH_REQUEST_DESCRIPTION, + }); + } + const response = await services.scopedClusterClient.asCurrentUser.eql.search( request ); const eqlSignalSearchEnd = performance.now(); - const eqlSearchDuration = makeFloatString(eqlSignalSearchEnd - eqlSignalSearchStart); - result.searchAfterTimes = [eqlSearchDuration]; + const eqlSearchDuration = eqlSignalSearchEnd - eqlSignalSearchStart; + result.searchAfterTimes = [makeFloatString(eqlSearchDuration)]; + + if (isLoggedRequestsEnabled && loggedRequests[0]) { + loggedRequests[0].duration = Math.round(eqlSearchDuration); + } let newSignals: Array> | undefined; @@ -198,8 +220,7 @@ export const eqlExecutor = async ({ responseActions: completeRule.ruleParams.responseActions, }); } - - return result; + return { result, ...(isLoggedRequestsEnabled ? { loggedRequests } : {}) }; } catch (error) { if ( typeof error.message === 'string' && @@ -211,7 +232,7 @@ export const eqlExecutor = async ({ } result.errors.push(error.message); result.success = false; - return result; + return { result, ...(isLoggedRequestsEnabled ? { loggedRequests } : {}) }; } }); }; 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 0dd2b0e50d4ba..1e5b1749e94f5 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 @@ -27,8 +27,11 @@ import { createEnrichEventsFunction } from '../utils/enrichments'; import { rowToDocument } from './utils'; import { fetchSourceDocuments } from './fetch_source_documents'; import { buildReasonMessageForEsqlAlert } from '../utils/reason_formatters'; - +import type { RulePreviewLoggedRequest } from '../../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; import type { RunOpts, SignalSource, CreateRuleAdditionalOptions } from '../types'; +import { logEsqlRequest } from '../utils/logged_requests'; +import * as i18n from '../translations'; + import { addToSearchAfterReturn, createSearchAfterReturnType, @@ -66,13 +69,14 @@ export const esqlExecutor = async ({ }: { runOpts: RunOpts; services: RuleExecutorServices; - state: object; + state: Record; spaceId: string; version: string; experimentalFeatures: ExperimentalFeatures; licensing: LicensingPluginSetup; scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService']; }) => { + const loggedRequests: RulePreviewLoggedRequest[] = []; const ruleParams = completeRule.ruleParams; /** * ES|QL returns results as a single page. max size of 10,000 @@ -80,91 +84,80 @@ export const esqlExecutor = async ({ * we don't want to overload ES/Kibana with large responses */ const ESQL_PAGE_SIZE_CIRCUIT_BREAKER = tuple.maxSignals * 3; + const isLoggedRequestsEnabled = state?.isLoggedRequestsEnabled ?? false; return withSecuritySpan('esqlExecutor', async () => { const result = createSearchAfterReturnType(); let size = tuple.maxSignals; - while ( - result.createdSignalsCount <= tuple.maxSignals && - size <= ESQL_PAGE_SIZE_CIRCUIT_BREAKER - ) { - const esqlRequest = buildEsqlSearchRequest({ - query: ruleParams.query, - from: tuple.from.toISOString(), - to: tuple.to.toISOString(), - size, - filters: [], - primaryTimestamp, - secondaryTimestamp, - exceptionFilter, - }); - - ruleExecutionLogger.debug(`ES|QL query request: ${JSON.stringify(esqlRequest)}`); - const exceptionsWarning = getUnprocessedExceptionsWarnings(unprocessedExceptions); - if (exceptionsWarning) { - result.warningMessages.push(exceptionsWarning); - } + try { + while ( + result.createdSignalsCount <= tuple.maxSignals && + size <= ESQL_PAGE_SIZE_CIRCUIT_BREAKER + ) { + const esqlRequest = buildEsqlSearchRequest({ + query: ruleParams.query, + from: tuple.from.toISOString(), + to: tuple.to.toISOString(), + size, + filters: [], + primaryTimestamp, + secondaryTimestamp, + exceptionFilter, + }); - const esqlSignalSearchStart = performance.now(); + if (isLoggedRequestsEnabled) { + loggedRequests.push({ + request: logEsqlRequest(esqlRequest), + description: i18n.ESQL_SEARCH_REQUEST_DESCRIPTION, + }); + } - const response = await performEsqlRequest({ - esClient: services.scopedClusterClient.asCurrentUser, - requestParams: esqlRequest, - }); + ruleExecutionLogger.debug(`ES|QL query request: ${JSON.stringify(esqlRequest)}`); + const exceptionsWarning = getUnprocessedExceptionsWarnings(unprocessedExceptions); + if (exceptionsWarning) { + result.warningMessages.push(exceptionsWarning); + } - const esqlSearchDuration = makeFloatString(performance.now() - esqlSignalSearchStart); - result.searchAfterTimes.push(esqlSearchDuration); + const esqlSignalSearchStart = performance.now(); - ruleExecutionLogger.debug(`ES|QL query request took: ${esqlSearchDuration}ms`); + const response = await performEsqlRequest({ + esClient: services.scopedClusterClient.asCurrentUser, + requestParams: esqlRequest, + }); - const isRuleAggregating = computeIsESQLQueryAggregating(completeRule.ruleParams.query); + const esqlSearchDuration = performance.now() - esqlSignalSearchStart; + result.searchAfterTimes.push(makeFloatString(esqlSearchDuration)); - const results = response.values - // slicing already processed results in previous iterations - .slice(size - tuple.maxSignals) - .map((row) => rowToDocument(response.columns, row)); + if (isLoggedRequestsEnabled && loggedRequests[0]) { + loggedRequests[0].duration = Math.round(esqlSearchDuration); + } - const index = getIndexListFromEsqlQuery(completeRule.ruleParams.query); + ruleExecutionLogger.debug(`ES|QL query request took: ${esqlSearchDuration}ms`); - const sourceDocuments = await fetchSourceDocuments({ - esClient: services.scopedClusterClient.asCurrentUser, - results, - index, - isRuleAggregating, - }); + const isRuleAggregating = computeIsESQLQueryAggregating(completeRule.ruleParams.query); - const isAlertSuppressionActive = await getIsAlertSuppressionActive({ - alertSuppression: completeRule.ruleParams.alertSuppression, - licensing, - }); + const results = response.values + // slicing already processed results in previous iterations + .slice(size - tuple.maxSignals) + .map((row) => rowToDocument(response.columns, row)); - const wrapHits = (events: Array>) => - wrapEsqlAlerts({ - events, - spaceId, - completeRule, - mergeStrategy, + const index = getIndexListFromEsqlQuery(completeRule.ruleParams.query); + + const sourceDocuments = await fetchSourceDocuments({ + esClient: services.scopedClusterClient.asCurrentUser, + results, + index, isRuleAggregating, - alertTimestampOverride, - ruleExecutionLogger, - publicBaseUrl, - tuple, + loggedRequests: isLoggedRequestsEnabled ? loggedRequests : undefined, }); - const syntheticHits: Array> = results.map((document) => { - const { _id, _version, _index, ...source } = document; - - return { - _source: source as SignalSource, - fields: _id ? sourceDocuments[_id]?.fields : {}, - _id: _id ?? '', - _index: _index ?? '', - }; - }); + const isAlertSuppressionActive = await getIsAlertSuppressionActive({ + alertSuppression: completeRule.ruleParams.alertSuppression, + licensing, + }); - if (isAlertSuppressionActive) { - const wrapSuppressedHits = (events: Array>) => - wrapSuppressedEsqlAlerts({ + const wrapHits = (events: Array>) => + wrapEsqlAlerts({ events, spaceId, completeRule, @@ -173,78 +166,108 @@ export const esqlExecutor = async ({ alertTimestampOverride, ruleExecutionLogger, publicBaseUrl, - primaryTimestamp, - secondaryTimestamp, tuple, }); - const bulkCreateResult = await bulkCreateSuppressedAlertsInMemory({ - enrichedEvents: syntheticHits, - toReturn: result, - wrapHits, - bulkCreate, - services, - ruleExecutionLogger, - tuple, - alertSuppression: completeRule.ruleParams.alertSuppression, - wrapSuppressedHits, - alertTimestampOverride, - alertWithSuppression, - experimentalFeatures, - buildReasonMessage: buildReasonMessageForEsqlAlert, - mergeSourceAndFields: true, - // passing 1 here since ES|QL does not support pagination - maxNumberOfAlertsMultiplier: 1, + const syntheticHits: Array> = results.map((document) => { + const { _id, _version, _index, ...source } = document; + + return { + _source: source as SignalSource, + fields: _id ? sourceDocuments[_id]?.fields : {}, + _id: _id ?? '', + _index: _index ?? '', + }; }); - ruleExecutionLogger.debug( - `Created ${bulkCreateResult.createdItemsCount} alerts. Suppressed ${bulkCreateResult.suppressedItemsCount} alerts` - ); + if (isAlertSuppressionActive) { + const wrapSuppressedHits = (events: Array>) => + wrapSuppressedEsqlAlerts({ + events, + spaceId, + completeRule, + mergeStrategy, + isRuleAggregating, + alertTimestampOverride, + ruleExecutionLogger, + publicBaseUrl, + primaryTimestamp, + secondaryTimestamp, + tuple, + }); - if (bulkCreateResult.alertsWereTruncated) { - result.warningMessages.push(getSuppressionMaxSignalsWarning()); - break; - } - } else { - const wrappedAlerts = wrapHits(syntheticHits); + const bulkCreateResult = await bulkCreateSuppressedAlertsInMemory({ + enrichedEvents: syntheticHits, + toReturn: result, + wrapHits, + bulkCreate, + services, + ruleExecutionLogger, + tuple, + alertSuppression: completeRule.ruleParams.alertSuppression, + wrapSuppressedHits, + alertTimestampOverride, + alertWithSuppression, + experimentalFeatures, + buildReasonMessage: buildReasonMessageForEsqlAlert, + mergeSourceAndFields: true, + // passing 1 here since ES|QL does not support pagination + maxNumberOfAlertsMultiplier: 1, + }); - const enrichAlerts = createEnrichEventsFunction({ - services, - logger: ruleExecutionLogger, - }); - const bulkCreateResult = await bulkCreate( - wrappedAlerts, - tuple.maxSignals - result.createdSignalsCount, - enrichAlerts - ); + ruleExecutionLogger.debug( + `Created ${bulkCreateResult.createdItemsCount} alerts. Suppressed ${bulkCreateResult.suppressedItemsCount} alerts` + ); - addToSearchAfterReturn({ current: result, next: bulkCreateResult }); - ruleExecutionLogger.debug(`Created ${bulkCreateResult.createdItemsCount} alerts`); + if (bulkCreateResult.alertsWereTruncated) { + result.warningMessages.push(getSuppressionMaxSignalsWarning()); + break; + } + } else { + const wrappedAlerts = wrapHits(syntheticHits); - if (bulkCreateResult.alertsWereTruncated) { - result.warningMessages.push(getMaxSignalsWarning()); - break; + const enrichAlerts = createEnrichEventsFunction({ + services, + logger: ruleExecutionLogger, + }); + const bulkCreateResult = await bulkCreate( + wrappedAlerts, + tuple.maxSignals - result.createdSignalsCount, + enrichAlerts + ); + + addToSearchAfterReturn({ current: result, next: bulkCreateResult }); + ruleExecutionLogger.debug(`Created ${bulkCreateResult.createdItemsCount} alerts`); + + if (bulkCreateResult.alertsWereTruncated) { + result.warningMessages.push(getMaxSignalsWarning()); + break; + } + } + + if (scheduleNotificationResponseActionsService) { + scheduleNotificationResponseActionsService({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: completeRule.ruleParams.responseActions, + }); } - } - if (scheduleNotificationResponseActionsService) { - scheduleNotificationResponseActionsService({ - signals: result.createdSignals, - signalsCount: result.createdSignalsCount, - responseActions: completeRule.ruleParams.responseActions, - }); - } - // no more results will be found - if (response.values.length < size) { - ruleExecutionLogger.debug( - `End of search: Found ${response.values.length} results with page size ${size}` - ); - break; + // no more results will be found + if (response.values.length < size) { + ruleExecutionLogger.debug( + `End of search: Found ${response.values.length} results with page size ${size}` + ); + break; + } + // ES|QL does not support pagination so we need to increase size of response to be able to catch all events + size += tuple.maxSignals; } - // ES|QL does not support pagination so we need to increase size of response to be able to catch all events - size += tuple.maxSignals; + } catch (error) { + result.errors.push(error.message); + result.success = false; } - return { ...result, state }; + return { ...result, state, ...(isLoggedRequestsEnabled ? { loggedRequests } : {}) }; }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/fetch_source_documents.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/fetch_source_documents.ts index 13828c0ed6770..7ee34183f0317 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/fetch_source_documents.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/fetch_source_documents.ts @@ -7,12 +7,16 @@ import type { ElasticsearchClient } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/types'; +import type { RulePreviewLoggedRequest } from '../../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; +import { logQueryRequest } from '../utils/logged_requests'; +import * as i18n from '../translations'; interface FetchSourceDocumentsArgs { isRuleAggregating: boolean; esClient: ElasticsearchClient; index: string[]; results: Array>; + loggedRequests?: RulePreviewLoggedRequest[]; } /** * fetches source documents by list of their ids @@ -24,6 +28,7 @@ export const fetchSourceDocuments = async ({ results, esClient, index, + loggedRequests, }: FetchSourceDocumentsArgs): Promise> => { const ids = results.reduce((acc, doc) => { if (doc._id) { @@ -47,16 +52,30 @@ export const fetchSourceDocuments = async ({ }, }; + const searchBody = { + query: idsQuery.query, + _source: false, + fields: ['*'], + }; + const ignoreUnavailable = true; + + if (loggedRequests) { + loggedRequests.push({ + request: logQueryRequest(searchBody, { index, ignoreUnavailable }), + description: i18n.FIND_SOURCE_DOCUMENTS_REQUEST_DESCRIPTION, + }); + } + const response = await esClient.search({ index, - body: { - query: idsQuery.query, - _source: false, - fields: ['*'], - }, - ignore_unavailable: true, + body: searchBody, + ignore_unavailable: ignoreUnavailable, }); + if (loggedRequests) { + loggedRequests[loggedRequests.length - 1].duration = response.took; + } + return response.hits.hits.reduce>( (acc, hit) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/translations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/translations.ts new file mode 100644 index 0000000000000..88445b0d41dc3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/translations.ts @@ -0,0 +1,29 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ESQL_SEARCH_REQUEST_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.esqlRuleType.esqlSearchRequestDescription', + { + defaultMessage: 'ES|QL request to find all matches', + } +); + +export const FIND_SOURCE_DOCUMENTS_REQUEST_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.esqlRuleType.findSourceDocumentsRequestDescription', + { + defaultMessage: 'Retrieve source documents when ES|QL query is not aggregable', + } +); + +export const EQL_SEARCH_REQUEST_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.esqlRuleType.eqlSearchRequestDescription', + { + defaultMessage: 'EQL request to find all matches', + } +); 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 a29beef7bbb20..78c0a729be10e 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 @@ -35,6 +35,7 @@ import type { TypeOfFieldMap } from '@kbn/rule-registry-plugin/common/field_map' import type { Filter, DataViewFieldBase } from '@kbn/es-query'; import type { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; +import type { RulePreviewLoggedRequest } from '../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; import type { RuleResponseAction } from '../../../../common/api/detection_engine/model/rule_response_actions'; import type { ConfigType } from '../../../config'; import type { SetupPlugins } from '../../../plugin'; @@ -74,6 +75,7 @@ export interface SecurityAlertTypeReturnValue { warning: boolean; warningMessages: string[]; suppressedAlertsCount?: number; + loggedRequests?: RulePreviewLoggedRequest[]; } export interface RunOpts { @@ -126,7 +128,12 @@ export type SecurityAlertType< services: PersistenceServices; runOpts: RunOpts; } - ) => Promise; + ) => Promise< + SearchAfterAndBulkCreateReturnType & { + state: TState; + loggedRequests?: RulePreviewLoggedRequest[]; + } + >; }; export interface CreateSecurityRuleTypeWrapperProps { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/index.ts new file mode 100644 index 0000000000000..7cea6e0d75fa6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export * from './log_esql'; +export * from './log_eql'; +export * from './log_query'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_eql.ts new file mode 100644 index 0000000000000..3f735e533b3b6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_eql.ts @@ -0,0 +1,19 @@ +/* + * 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 { EqlSearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +export const logEqlRequest = (request: EqlSearchRequest): string => { + const allowNoIndices = + request.allow_no_indices != null ? `?allow_no_indices=${request.allow_no_indices}` : ''; + + return `POST /${request.index}/_eql/search${allowNoIndices}\n${JSON.stringify( + request.body, + null, + 2 + )}`; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_esql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_esql.ts new file mode 100644 index 0000000000000..ea2d652dc5e5e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_esql.ts @@ -0,0 +1,15 @@ +/* + * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +export const logEsqlRequest = (esqlRequest: { + query: string; + filter: QueryDslQueryContainer; +}): string => { + return `POST _query\n${JSON.stringify(esqlRequest, null, 2)}`; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_query.ts new file mode 100644 index 0000000000000..8ea992793e31d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_query.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + QueryDslQueryContainer, + SearchSourceConfig, + Indices, + Fields, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +interface SearchRequest { + query?: QueryDslQueryContainer; + _source?: SearchSourceConfig; + fields?: Fields; +} + +interface LogQueryRequestParams { + index: Indices; + ignoreUnavailable?: boolean; +} + +export const logQueryRequest = ( + searchRequest: SearchRequest, + { index, ignoreUnavailable = false }: LogQueryRequestParams +): string => { + return `POST /${index}/_search?ignore_unavailable=${ignoreUnavailable}\n${JSON.stringify( + searchRequest, + null, + 2 + )}`; +}; diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 5bf7ac87908d3..44f928e98bd0f 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -116,7 +116,10 @@ import { PreviewRiskScoreRequestBodyInput } from '@kbn/security-solution-plugin/ import { ReadAlertsMigrationStatusRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/read_signals_migration_status/read_signals_migration_status.gen'; import { ReadRuleRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/read_rule/read_rule_route.gen'; import { ResolveTimelineRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/resolve_timeline/resolve_timeline_route.gen'; -import { RulePreviewRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_preview/rule_preview.gen'; +import { + RulePreviewRequestQueryInput, + RulePreviewRequestBodyInput, +} from '@kbn/security-solution-plugin/common/api/detection_engine/rule_preview/rule_preview.gen'; import { SearchAlertsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/query_signals/query_signals_route.gen'; import { SetAlertAssigneesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_assignees/set_alert_assignees_route.gen'; import { SetAlertsStatusRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/set_signal_status/set_signals_status_route.gen'; @@ -1058,7 +1061,8 @@ detection engine rules. .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') - .send(props.body as object); + .send(props.body as object) + .query(props.query); }, scheduleRiskEngineNow() { return supertest @@ -1394,6 +1398,7 @@ export interface ResolveTimelineProps { query: ResolveTimelineRequestQueryInput; } export interface RulePreviewProps { + query: RulePreviewRequestQueryInput; body: RulePreviewRequestBodyInput; } export interface SearchAlertsProps { diff --git a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts index 6cd2702857e0f..705c0b8686dd0 100644 --- a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts @@ -82,6 +82,7 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s '--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true', `--xpack.securitySolution.enableExperimental=${JSON.stringify([ 'previewTelemetryUrlEnabled', + 'loggingRequestsEnabled', 'riskScoringPersistence', 'riskScoringRoutesEnabled', 'manualRuleRunEnabled', diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts index 6691a781d4e1e..8f64a859b7002 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts @@ -17,6 +17,9 @@ export default createTestConfig({ 'testing_ignored.constant', '/testing_regex*/', ])}`, // See tests within the file "ignore_fields.ts" which use these values in "alertIgnoreFields" - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`, + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'manualRuleRunEnabled', + 'loggingRequestsEnabled', + ])}`, ], }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts index 53b2843399c62..aff2ccc6bccb3 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts @@ -1189,5 +1189,32 @@ export default ({ getService }: FtrProviderContext) => { expect(updatedAlerts.hits.hits[0]._source?.[ALERT_SUPPRESSION_DOCS_COUNT]).equal(1); }); }); + + // skipped on MKI since feature flags are not supported there + describe('@skipInServerlessMKI preview logged requests', () => { + it('should not return requests property when not enabled', async () => { + const { logs } = await previewRule({ + supertest, + rule: getEqlRuleForAlertTesting(['auditbeat-*']), + }); + + expect(logs[0].requests).equal(undefined); + }); + it('should return requests property when enable_logged_requests set to true', async () => { + const { logs } = await previewRule({ + supertest, + rule: getEqlRuleForAlertTesting(['auditbeat-*']), + enableLoggedRequests: true, + }); + + const requests = logs[0].requests; + + expect(requests).to.have.length(1); + expect(requests![0].description).to.be('EQL request to find all matches'); + expect(requests![0].request).to.contain( + 'POST /auditbeat-*/_eql/search?allow_no_indices=true' + ); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts index 9fbda25bdae64..166a62b9b08ad 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts @@ -1408,5 +1408,63 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); + + // skipped on MKI since feature flags are not supported there + describe('@skipInServerlessMKI preview logged requests', () => { + let rule: EsqlRuleCreateProps; + let id: string; + beforeEach(async () => { + id = uuidv4(); + const interval: [string, string] = ['2020-10-28T06:00:00.000Z', '2020-10-28T06:10:00.000Z']; + const doc1 = { agent: { name: 'test-1' } }; + + rule = { + ...getCreateEsqlRulesSchemaMock('rule-1', true), + query: `from ecs_compliant metadata _id ${internalIdPipe( + id + )} | where agent.name=="test-1"`, + from: 'now-1h', + interval: '1h', + }; + + await indexEnhancedDocuments({ documents: [doc1], interval, id }); + }); + + it('should not return requests property when not enabled', async () => { + const { logs } = await previewRule({ + supertest, + rule, + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + }); + + expect(logs[0]).not.toHaveProperty('requests'); + }); + it('should return requests property when enable_logged_requests set to true', async () => { + const { logs } = await previewRule({ + supertest, + rule, + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + enableLoggedRequests: true, + }); + + const requests = logs[0].requests; + expect(requests).toHaveLength(2); + + expect(requests).toHaveProperty('0.description', 'ES|QL request to find all matches'); + expect(requests).toHaveProperty('0.duration', expect.any(Number)); + expect(requests![0].request).toContain( + `"query": "from ecs_compliant metadata _id | where id==\\\"${id}\\\" | where agent.name==\\\"test-1\\\" | limit 101",` + ); + + expect(requests).toHaveProperty( + '1.description', + 'Retrieve source documents when ES|QL query is not aggregable' + ); + expect(requests).toHaveProperty('1.duration', expect.any(Number)); + expect(requests![1].request).toContain( + 'POST /ecs_compliant/_search?ignore_unavailable=true' + ); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts index a601edde98168..79668b902f5d0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts @@ -26,11 +26,13 @@ export const previewRule = async ({ rule, invocationCount = 1, timeframeEnd = new Date(), + enableLoggedRequests, }: { supertest: SuperTest.Agent; rule: RuleCreateProps; invocationCount?: number; timeframeEnd?: Date; + enableLoggedRequests?: boolean; }): Promise<{ previewId: string; logs: RulePreviewLogs[]; @@ -43,6 +45,7 @@ export const previewRule = async ({ }; const response = await supertest .post(DETECTION_ENGINE_RULES_PREVIEW) + .query(enableLoggedRequests ? { enable_logged_requests: true } : {}) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') .send(previewRequest) diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 58d873369d99d..88752eb1b5f93 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -44,7 +44,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // See https://github.com/elastic/kibana/pull/125396 for details '--xpack.alerting.rules.minimumScheduleInterval.value=1s', '--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true', - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`, + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'manualRuleRunEnabled', + 'loggingRequestsEnabled', + ])}`, // mock cloud to enable the guided onboarding tour in e2e tests '--xpack.cloud.id=test', `--home.disableWelcomeScreen=true`, diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts new file mode 100644 index 0000000000000..ce298bafbfea0 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts @@ -0,0 +1,85 @@ +/* + * 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 { getEsqlRule, getSimpleCustomQueryRule } from '../../../../objects/rule'; + +import { + PREVIEW_LOGGED_REQUEST_DESCRIPTION, + PREVIEW_LOGGED_REQUEST_CODE_BLOCK, + PREVIEW_LOGGED_REQUESTS_CHECKBOX, + RULES_CREATION_PREVIEW_REFRESH_BUTTON, +} from '../../../../screens/create_new_rule'; + +import { createRule } from '../../../../tasks/api_calls/rules'; + +import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; +import { + checkEnableLoggedRequests, + submitRulePreview, + toggleLoggedRequestsAccordion, + toggleLoggedRequestsItemAccordion, +} from '../../../../tasks/create_new_rule'; +import { login } from '../../../../tasks/login'; + +import { visitEditRulePage } from '../../../../tasks/edit_rule'; + +const expectedValidEsqlQuery = 'from auditbeat* METADATA _id'; + +describe( + 'Detection rules, preview', + { + // Currently FF are not supported on MKI environments, so this test should be skipped from MKI environments. + // Once `manualRuleRunEnabled` FF is removed, we can remove `@skipInServerlessMKI` as well + tags: ['@ess', '@serverless', '@skipInServerlessMKI'], + env: { + kbnServerArgs: [ + `--xpack.securitySolution.enableExperimental=${JSON.stringify(['loggingRequestsEnabled'])}`, + ], + }, + }, + () => { + beforeEach(() => { + login(); + deleteAlertsAndRules(); + }); + + describe('supports preview logged requests', () => { + beforeEach(() => { + createRule({ ...getEsqlRule(), query: expectedValidEsqlQuery }).then((createdRule) => { + visitEditRulePage(createdRule.body.id); + }); + }); + + it('shows preview logged requests', () => { + checkEnableLoggedRequests(); + submitRulePreview(); + + toggleLoggedRequestsAccordion(); + toggleLoggedRequestsItemAccordion(); + + cy.get(PREVIEW_LOGGED_REQUEST_DESCRIPTION) + .first() + .contains('ES|QL request to find all matches'); + + cy.get(PREVIEW_LOGGED_REQUEST_CODE_BLOCK).first().contains(expectedValidEsqlQuery); + }); + }); + + describe('does not support preview logged requests', () => { + beforeEach(() => { + createRule(getSimpleCustomQueryRule()).then((createdRule) => { + visitEditRulePage(createdRule.body.id); + }); + }); + + it('does not show preview logged requests checkbox', () => { + cy.get(RULES_CREATION_PREVIEW_REFRESH_BUTTON).should('be.visible'); + cy.get(PREVIEW_LOGGED_REQUESTS_CHECKBOX).should('not.exist'); + }); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts index 72d1104985d77..a191fc22aa339 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts @@ -296,3 +296,19 @@ export const RULE_INDICES = '[data-test-subj="detectionEngineStepDefineRuleIndices"] [data-test-subj="comboBoxInput"]'; export const ALERTS_INDEX_BUTTON = 'span[title=".alerts-security.alerts-default"] button'; + +export const PREVIEW_SUBMIT_BUTTON = '[data-test-subj="previewSubmitButton"]'; + +export const PREVIEW_LOGGED_REQUESTS_CHECKBOX = '[data-test-subj="show-elasticsearch-requests"]'; + +export const PREVIEW_LOGGED_REQUESTS_ACCORDION_BUTTON = + '[data-test-subj="preview-logged-requests-accordion"] button'; + +export const PREVIEW_LOGGED_REQUESTS_ITEM_ACCORDION_BUTTON = + '[data-test-subj="preview-logged-requests-item-accordion"] button'; + +export const PREVIEW_LOGGED_REQUEST_DESCRIPTION = + '[data-test-subj="preview-logged-request-description"]'; + +export const PREVIEW_LOGGED_REQUEST_CODE_BLOCK = + '[data-test-subj="preview-logged-request-code-block"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts index ecc37de80b456..68dc2cfffd908 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts @@ -130,6 +130,9 @@ import { RELATED_INTEGRATION_COMBO_BOX_INPUT, SAVE_WITH_ERRORS_MODAL, SAVE_WITH_ERRORS_MODAL_CONFIRM_BTN, + PREVIEW_LOGGED_REQUESTS_ACCORDION_BUTTON, + PREVIEW_LOGGED_REQUESTS_ITEM_ACCORDION_BUTTON, + PREVIEW_LOGGED_REQUESTS_CHECKBOX, } from '../screens/create_new_rule'; import { INDEX_SELECTOR, @@ -996,3 +999,20 @@ export const uncheckLoadQueryDynamically = () => { export const openAddFilterPopover = () => { cy.get(QUERY_BAR_ADD_FILTER).click(); }; + +export const checkEnableLoggedRequests = () => { + cy.get(PREVIEW_LOGGED_REQUESTS_CHECKBOX).click(); + cy.get(PREVIEW_LOGGED_REQUESTS_CHECKBOX).should('be.checked'); +}; + +export const submitRulePreview = () => { + cy.get(RULES_CREATION_PREVIEW_REFRESH_BUTTON).click(); +}; + +export const toggleLoggedRequestsAccordion = () => { + cy.get(PREVIEW_LOGGED_REQUESTS_ACCORDION_BUTTON).first().click(); +}; + +export const toggleLoggedRequestsItemAccordion = () => { + cy.get(PREVIEW_LOGGED_REQUESTS_ITEM_ACCORDION_BUTTON).should('be.visible').first().click(); +}; diff --git a/x-pack/test/security_solution_cypress/serverless_config.ts b/x-pack/test/security_solution_cypress/serverless_config.ts index 226f2db6dbbc0..13877fcbf5af4 100644 --- a/x-pack/test/security_solution_cypress/serverless_config.ts +++ b/x-pack/test/security_solution_cypress/serverless_config.ts @@ -34,7 +34,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { { product_line: 'endpoint', product_tier: 'complete' }, { product_line: 'cloud', product_tier: 'complete' }, ])}`, - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`, + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'manualRuleRunEnabled', + 'loggingRequestsEnabled', + ])}`, '--csp.strict=false', '--csp.warnLegacyBrowsers=false', ],