mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[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:
parent
e524ed6a1a
commit
60176bcffd
46 changed files with 1281 additions and 178 deletions
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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/
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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';
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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};`;
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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 },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 } : {}) };
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 } : {}) };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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 } : {}) };
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
|
@ -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
|
||||
)}`;
|
||||
};
|
|
@ -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)}`;
|
||||
};
|
|
@ -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
|
||||
)}`;
|
||||
};
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
])}`,
|
||||
],
|
||||
});
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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"]';
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue