[Security Solution][Detection Engine] log ES requests when running rule preview (#191107)

## 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
<img width="733" alt="Screenshot 2024-09-11 at 12 39 07"
src="https://github.com/user-attachments/assets/c2b1304d-6f93-4e8e-92f9-a6a0b53cefc7">

#### List of executions and code blocks
<img width="770" alt="Screenshot 2024-09-11 at 12 38 23"
src="https://github.com/user-attachments/assets/48b5aa12-174c-46f5-b0bc-a141833b225b">




### 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>
This commit is contained in:
Vitalii Dmyterko 2024-09-19 14:45:41 +01:00 committed by GitHub
parent e524ed6a1a
commit 60176bcffd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 1281 additions and 178 deletions

View file

@ -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<typeof RulePreviewLoggedRequest>;
export const RulePreviewLoggedRequest = z.object({
request: NonEmptyString,
description: NonEmptyString.optional(),
duration: z.number().int().optional(),
});
export type RulePreviewLogs = z.infer<typeof RulePreviewLogs>;
export const RulePreviewLogs = z.object({
errors: z.array(NonEmptyString),
@ -43,8 +51,18 @@ export const RulePreviewLogs = z.object({
*/
duration: z.number().int(),
startedAt: NonEmptyString.optional(),
requests: z.array(RulePreviewLoggedRequest).optional(),
});
export type RulePreviewRequestQuery = z.infer<typeof RulePreviewRequestQuery>;
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<typeof RulePreviewRequestQuery>;
export type RulePreviewRequestBody = z.infer<typeof RulePreviewRequestBody>;
export const RulePreviewRequestBody = z.discriminatedUnion('type', [
EqlRuleCreateProps.merge(RulePreviewParams),

View file

@ -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

View file

@ -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 {

View file

@ -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
*/

View file

@ -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:

View file

@ -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:

View file

@ -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,
},
],
},
];

View file

@ -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(
<TestProviders>
<RulePreview
{...defaultProps}
defineRuleData={{ ...defaultProps.defineRuleData, ruleType }}
/>
</TestProviders>
);
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(
<TestProviders>
<RulePreview
{...defaultProps}
defineRuleData={{ ...defaultProps.defineRuleData, ruleType }}
/>
</TestProviders>
);
expect(screen.queryByTestId('show-elasticsearch-requests')).toBeNull();
});
});
doNotSupportLoggedRequests.forEach((ruleType) => {
test(`does not render "Show Elasticsearch requests" for ${ruleType} rule type`, () => {
render(
<TestProviders>
<RulePreview
{...defaultProps}
defineRuleData={{ ...defaultProps.defineRuleData, ruleType }}
/>
</TestProviders>
);
expect(screen.queryByTestId('show-elasticsearch-requests')).toBeNull();
});
});
});

View file

@ -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<RulePreviewProps> = ({
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<RulePreviewProps> = ({
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<RulePreviewProps> = ({
scheduleRuleData: previewData.scheduleRuleData,
exceptionsList,
timeframeOptions: previewData.timeframeOptions,
enableLoggedRequests: previewData.enableLoggedRequests,
});
const { startTransaction } = useStartTransaction();
@ -185,9 +197,18 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
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<RulePreviewProps> = ({
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
{isLoggingRequestsFeatureEnabled &&
RULE_TYPES_SUPPORTING_LOGGED_REQUESTS.includes(ruleType) ? (
<EuiFormRow>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive>
<EuiFlexItem grow>
<EuiCheckbox
data-test-subj="show-elasticsearch-requests"
id="showElasticsearchRequests"
label={i18n.ENABLED_LOGGED_REQUESTS_CHECKBOX}
checked={showElasticsearchRequests}
onChange={() => {
setShowElasticsearchRequests(!showElasticsearchRequests);
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
) : null}
<EuiSpacer size="l" />
{isPreviewRequestInProgress && <LoadingHistogram />}
{!isPreviewRequestInProgress && previewId && spaceId && (
@ -273,7 +312,12 @@ const RulePreviewComponent: React.FC<RulePreviewProps> = ({
timeframeOptions={previewData.timeframeOptions}
/>
)}
<PreviewLogs logs={logs} hasNoiseWarning={hasNoiseWarning} isAborted={isAborted} />
<PreviewLogs
logs={logs}
hasNoiseWarning={hasNoiseWarning}
isAborted={isAborted}
showElasticsearchRequests={showElasticsearchRequests}
/>
</div>
);
};

View file

@ -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(<LoggedRequests logs={[]} />, { wrapper: TestProviders });
expect(screen.queryByTestId('preview-logged-requests-accordion')).toBeNull();
});
it('should open accordion on click and render list of request items', async () => {
render(<LoggedRequests logs={previewLogs} />, { 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(<LoggedRequests logs={previewLogs} />, { 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/
);
});
});

View file

@ -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(
() => (
<>
<EuiSpacer size="m" />
{logs.map((log) => (
<React.Fragment key={log.startedAt}>
<LoggedRequestsItem {...log} />
</React.Fragment>
))}
</>
),
[logs]
);
if (logs.length === 0) {
return null;
}
return (
<>
<OptimizedAccordion
id="preview-logged-requests-accordion"
data-test-subj="preview-logged-requests-accordion"
buttonContent={i18n.LOGGED_REQUESTS_ACCORDION_BUTTON}
borders="horizontal"
css={css`
${cssStyles}
`}
>
{AccordionContent}
</OptimizedAccordion>
</>
);
};
export const LoggedRequests = React.memo(LoggedRequestsComponent);
LoggedRequests.displayName = 'LoggedRequests';

View file

@ -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<PropsWithChildren<RulePreviewLogs>> = ({
startedAt,
duration,
requests,
}) => {
const paddingLarge = useEuiPaddingSize('l');
const cssStyles = useAccordionStyling();
return (
<OptimizedAccordion
data-test-subj="preview-logged-requests-item-accordion"
buttonContent={
<>
{startedAt ? (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.queryPreview.loggedRequestItemAccordionButtonLabel"
defaultMessage="Rule execution started at {time}."
values={{ time: <PreferenceFormattedDate value={new Date(startedAt)} /> }}
/>
) : (
i18n.LOGGED_REQUEST_ITEM_ACCORDION_UNKNOWN_TIME_BUTTON
)}
{`[${duration}ms]`}
</>
}
id={`ruleExecution-${startedAt}`}
css={css`
margin-left: ${paddingLarge};
${cssStyles}
`}
>
{(requests ?? []).map((request, key) => (
<EuiFlexItem
key={key}
css={css`
padding-left: ${paddingLarge};
`}
>
<EuiSpacer size="l" />
<span data-test-subj="preview-logged-request-description">
{request?.description ?? null} {request?.duration ? `[${request.duration}ms]` : null}
</span>
<EuiSpacer size="s" />
<EuiCodeBlock
language="json"
isCopyable
overflowHeight={300}
isVirtualized
data-test-subj="preview-logged-request-code-block"
>
{request.request}
</EuiCodeBlock>
</EuiFlexItem>
))}
</OptimizedAccordion>
);
};
export const LoggedRequestsItem = React.memo(LoggedRequestsItemComponent);
LoggedRequestsItem.displayName = 'LoggedRequestsItem';

View file

@ -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(
<OptimizedAccordion id="test" buttonContent={'accordion button'}>
<span>{'content'}</span>
</OptimizedAccordion>
);
expect(screen.queryByText('content')).toBeNull();
});
it('should render children content if accordion initially opened', () => {
render(
<OptimizedAccordion id="test" buttonContent={'accordion button'} forceState="open">
<span>{'content'}</span>
</OptimizedAccordion>
);
expect(screen.getByText('content')).toBeInTheDocument();
});
it('should render children content when accordion opened', async () => {
render(
<OptimizedAccordion id="test" buttonContent={'accordion button'}>
<span>{'content'}</span>
</OptimizedAccordion>
);
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(
<OptimizedAccordion id="test" buttonContent={'accordion button'}>
<span>{'content'}</span>
</OptimizedAccordion>
);
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();
});
});

View file

@ -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<EuiAccordionProps> = ({ children, ...props }) => {
const [trigger, setTrigger] = useState<'closed' | 'open'>('closed');
const [isRendered, setIsRendered] = useState<boolean>(false);
const onToggle = (isOpen: boolean) => {
const newState = isOpen ? 'open' : 'closed';
if (isOpen) {
setIsRendered(true);
}
setTrigger(newState);
};
return (
<EuiAccordion {...props} forceState={trigger} onToggle={onToggle}>
{isRendered || props.forceState === 'open' ? children : null}
</EuiAccordion>
);
};
export const OptimizedAccordion = React.memo(OptimizedAccordionComponent);
OptimizedAccordion.displayName = 'OptimizedAccordion';

View file

@ -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<PreviewLogsProps> = ({ logs, hasNoiseWarning, isAborted }) => {
const PreviewLogsComponent: React.FC<PreviewLogsProps> = ({
logs,
hasNoiseWarning,
isAborted,
showElasticsearchRequests,
}) => {
const sortedLogs = useMemo(
() =>
logs.reduce<{
@ -66,6 +76,7 @@ const PreviewLogsComponent: React.FC<PreviewLogsProps> = ({ logs, hasNoiseWarnin
<LogAccordion logs={sortedLogs.warnings}>
{isAborted ? <CustomWarning message={i18n.PREVIEW_TIMEOUT_WARNING} /> : null}
</LogAccordion>
{showElasticsearchRequests ? <LoggedRequests logs={logs} /> : null}
</>
);
};
@ -74,6 +85,8 @@ export const PreviewLogs = React.memo(PreviewLogsComponent);
PreviewLogs.displayName = 'PreviewLogs';
const LogAccordion: FC<PropsWithChildren<LogAccordionProps>> = ({ logs, isError, children }) => {
const cssStyles = useAccordionStyling();
const firstLog = logs[0];
if (!(children || firstLog)) return null;
@ -96,6 +109,10 @@ const LogAccordion: FC<PropsWithChildren<LogAccordionProps>> = ({ 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) => (
<CalloutGroup
@ -108,7 +125,6 @@ const LogAccordion: FC<PropsWithChildren<LogAccordionProps>> = ({ logs, isError,
))}
</EuiAccordion>
) : null}
<EuiSpacer size="m" />
</>
);
};

View file

@ -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,

View file

@ -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};`;
};

View file

@ -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<RulePreviewLogs[]>(response.logs ?? []);
const [isAborted, setIsAborted] = useState<boolean>(!!response.isAborted);

View file

@ -27,8 +27,10 @@ const emptyPreviewRule: RulePreviewResponse = {
export const usePreviewRule = ({
timeframeOptions,
enableLoggedRequests,
}: {
timeframeOptions: TimeframePreviewOptions;
enableLoggedRequests?: boolean;
}) => {
const [rule, setRule] = useState<RuleCreateProps | null>(null);
const [response, setResponse] = useState<RulePreviewResponse>(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 };
};

View file

@ -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 },
})
);
});

View file

@ -150,6 +150,7 @@ export const patchRule = async ({
*/
export const previewRule = async ({
rule,
enableLoggedRequests,
signal,
}: PreviewRulesProps): Promise<RulePreviewResponse> =>
KibanaServices.get().http.fetch<RulePreviewResponse>(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,
});
/**

View file

@ -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;
}

View file

@ -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<IKibanaResponse<RulePreviewResponse>> => {
const siemResponse = buildSiemResponse(response);
@ -143,7 +152,9 @@ export const previewRulesRoute = (
const username = security?.authc.getCurrentUser(request)?.username;
const loggedStatusChanges: Array<RuleExecutionContext & StatusChangeArgs> = [];
const previewRuleExecutionLogger = createPreviewRuleExecutionLogger(loggedStatusChanges);
const runState: Record<string, unknown> = {};
const runState: Record<string, unknown> = {
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;

View file

@ -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: {

View file

@ -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 } : {}) };
},
};
};

View file

@ -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,

View file

@ -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<string, unknown>;
scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService'];
}
@ -90,10 +94,17 @@ export const eqlExecutor = async ({
alertWithSuppression,
isAlertSuppressionActive,
experimentalFeatures,
state,
scheduleNotificationResponseActionsService,
}: EqlExecutorParams): Promise<SearchAfterAndBulkCreateReturnType> => {
}: 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<SignalSource>(
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<WrappedFieldsLatest<BaseFieldsLatest>> | 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 } : {}) };
}
});
};

View file

@ -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<EsqlRuleParams>;
services: RuleExecutorServices<AlertInstanceState, AlertInstanceContext, 'default'>;
state: object;
state: Record<string, unknown>;
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,10 +84,12 @@ 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;
try {
while (
result.createdSignalsCount <= tuple.maxSignals &&
size <= ESQL_PAGE_SIZE_CIRCUIT_BREAKER
@ -99,6 +105,13 @@ export const esqlExecutor = async ({
exceptionFilter,
});
if (isLoggedRequestsEnabled) {
loggedRequests.push({
request: logEsqlRequest(esqlRequest),
description: i18n.ESQL_SEARCH_REQUEST_DESCRIPTION,
});
}
ruleExecutionLogger.debug(`ES|QL query request: ${JSON.stringify(esqlRequest)}`);
const exceptionsWarning = getUnprocessedExceptionsWarnings(unprocessedExceptions);
if (exceptionsWarning) {
@ -112,8 +125,12 @@ export const esqlExecutor = async ({
requestParams: esqlRequest,
});
const esqlSearchDuration = makeFloatString(performance.now() - esqlSignalSearchStart);
result.searchAfterTimes.push(esqlSearchDuration);
const esqlSearchDuration = performance.now() - esqlSignalSearchStart;
result.searchAfterTimes.push(makeFloatString(esqlSearchDuration));
if (isLoggedRequestsEnabled && loggedRequests[0]) {
loggedRequests[0].duration = Math.round(esqlSearchDuration);
}
ruleExecutionLogger.debug(`ES|QL query request took: ${esqlSearchDuration}ms`);
@ -131,6 +148,7 @@ export const esqlExecutor = async ({
results,
index,
isRuleAggregating,
loggedRequests: isLoggedRequestsEnabled ? loggedRequests : undefined,
});
const isAlertSuppressionActive = await getIsAlertSuppressionActive({
@ -226,6 +244,7 @@ export const esqlExecutor = async ({
break;
}
}
if (scheduleNotificationResponseActionsService) {
scheduleNotificationResponseActionsService({
signals: result.createdSignals,
@ -244,7 +263,11 @@ export const esqlExecutor = async ({
// 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 } : {}) };
});
};

View file

@ -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<Record<string, string | null>>;
loggedRequests?: RulePreviewLoggedRequest[];
}
/**
* fetches source documents by list of their ids
@ -24,6 +28,7 @@ export const fetchSourceDocuments = async ({
results,
esClient,
index,
loggedRequests,
}: FetchSourceDocumentsArgs): Promise<Record<string, { fields: estypes.SearchHit['fields'] }>> => {
const ids = results.reduce<string[]>((acc, doc) => {
if (doc._id) {
@ -47,15 +52,29 @@ export const fetchSourceDocuments = async ({
},
};
const response = await esClient.search({
index,
body: {
const searchBody = {
query: idsQuery.query,
_source: false,
fields: ['*'],
},
ignore_unavailable: true,
};
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: searchBody,
ignore_unavailable: ignoreUnavailable,
});
if (loggedRequests) {
loggedRequests[loggedRequests.length - 1].duration = response.took;
}
return response.hits.hits.reduce<Record<string, { fields: estypes.SearchHit['fields'] }>>(
(acc, hit) => {

View file

@ -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',
}
);

View file

@ -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<TState extends RuleTypeState> {
warning: boolean;
warningMessages: string[];
suppressedAlertsCount?: number;
loggedRequests?: RulePreviewLoggedRequest[];
}
export interface RunOpts<TParams extends RuleParams> {
@ -126,7 +128,12 @@ export type SecurityAlertType<
services: PersistenceServices;
runOpts: RunOpts<TParams>;
}
) => Promise<SearchAfterAndBulkCreateReturnType & { state: TState }>;
) => Promise<
SearchAfterAndBulkCreateReturnType & {
state: TState;
loggedRequests?: RulePreviewLoggedRequest[];
}
>;
};
export interface CreateSecurityRuleTypeWrapperProps {

View file

@ -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';

View file

@ -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
)}`;
};

View file

@ -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)}`;
};

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type {
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
)}`;
};

View file

@ -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 {

View file

@ -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',

View file

@ -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',
])}`,
],
});

View file

@ -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'
);
});
});
});
};

View file

@ -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'
);
});
});
});
};

View file

@ -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)

View file

@ -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`,

View file

@ -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');
});
});
}
);

View file

@ -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"]';

View file

@ -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();
};

View file

@ -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',
],