mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[8.x] [Security Solution] Add EQL query editable component with EQL options fields (#199115) (#201314)
# Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution] Add EQL query editable component with EQL options fields (#199115)](https://github.com/elastic/kibana/pull/199115) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Maxim Palenov","email":"maxim.palenov@elastic.co"},"sourceCommit":{"committedDate":"2024-11-22T07:27:40Z","message":"[Security Solution] Add EQL query editable component with EQL options fields (#199115)\n\n**Partially addresses:** https://github.com/elastic/kibana/issues/171520\r\n\r\n## Summary\r\n\r\nThis PR adds is built on top of https://github.com/elastic/kibana/pull/193828 and https://github.com/elastic/kibana/pull/196948 and adds an EQL Query editable component with EQL Options fields (`event_category_override`, `timestamp_field` and `tiebreaker_field`) for Three Way Diff tab's final edit side of the upgrade prebuilt rule workflow.\r\n\r\n## Details\r\n\r\nThis PR make a set of changes to make existing EQL Query bar component easily reusable and type safe when used in forms. In particular the following was done\r\n\r\n- EQL query bar was wrapped in `EqlQueryEdit` component with `UseField` inside. It helps to make it type safe avoiding issues like passing invalid types to `EqlQueryBar`. `UseField` types component properties as `Record<string, any>` so potentially any refactoring can break some functionality. For example code in Timeline passes `DataViewSpec` where `DataViewBase` is expected while these two types aren't fully compatible.\r\n- Validation was added directly to `EqlQueryEdit`. Passing field configuration to `UseField` rewrites field configuration defined in from schema. It leads to cases when validation is defined in both form schema and as a field configuration for `UseFields`. Additionally we can reduce reusing complexity by incapsulating absolutely required validation in `EqlQueryEdit` component.\r\n- Empty string `tiebreakerField` was removed in Timelines. `tiebreakerField` is part of EQL options used for EQL validation. EQL validation endpoint `/internal/search/eql` returns an error when an empty string provided for `tiebreakerField`. This problem didn't surface earlier since It looks like EQL options weren't provided correctly before this PR. Timeline EQL validation requests were sent without EQL options.\r\n\r\n## How to test\r\n\r\nThe simplest way to test is via patching installed prebuilt rules via Rule Patch API. Please follow steps below\r\n\r\n- Ensure the `prebuiltRulesCustomizationEnabled` feature flag is enabled\r\n- Run Kibana locally\r\n- Install an EQL prebuilt rule, e.g. `Potential Code Execution via Postgresql` with rule_id `2a692072-d78d-42f3-a48a-775677d79c4e`\r\n- Patch the installed rule by running a query below\r\n\r\n```bash\r\ncurl -X PATCH --user elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123' -H \"elastic-api-version: 2023-10-31\" -d '{\"rule_id\":\"2a692072-d78d-42f3-a48a-775677d79c4e\",\"version\":1,\"query\":\"process where process.name == \\\"cmd.exe\\\"\",\"language\":\"eql\",\"event_category_override\": \"test\",\"timestamp_field\": \"@timestamp\",\"tiebreaker_field\": \"tiebreaker\"}' http://localhost:5601/kbn/api/detection_engine/rules\r\n```\r\n\r\n- Open `Detection Rules (SIEM)` Page -> `Rule Updates` -> click on `Potential Code Execution via Postgresql` rule -> expand `EQL Query` to see EQL Query -> press `Edit` button\r\n\r\n## Screenshots\r\n\r\n- EQL Query in Prebuilt Rules Update workflow\r\n<img width=\"2560\" alt=\"image\" src=\"https://github.com/user-attachments/assets/59d157b2-6aca-4b21-95d0-f71a2e174df2\">\r\n\r\n- event_category_override + tiebreaker_field + timestamp_field (aka EQL options) in Prebuilt Rules Update workflow\r\n<img width=\"2552\" alt=\"image\" src=\"https://github.com/user-attachments/assets/1886d3b4-98f9-40a7-954c-2a6d4b8e925a\">\r\n\r\n- Examples of invalid EQL\r\n<img width=\"2560\" alt=\"image\" src=\"https://github.com/user-attachments/assets/d584deca-7903-45c5-9499-718552df441c\">\r\n\r\n<img width=\"2548\" alt=\"image\" src=\"https://github.com/user-attachments/assets/b734e22c-ab62-4624-85d0-e4e6dbb9d523\">","sha":"c0c803c8830c10f1df1b204a7d7b859f1f584991","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Detections and Resp","Team: SecuritySolution","Team:Detection Rule Management","Feature:Prebuilt Detection Rules","backport:version","v8.18.0"],"number":199115,"url":"https://github.com/elastic/kibana/pull/199115","mergeCommit":{"message":"[Security Solution] Add EQL query editable component with EQL options fields (#199115)\n\n**Partially addresses:** https://github.com/elastic/kibana/issues/171520\r\n\r\n## Summary\r\n\r\nThis PR adds is built on top of https://github.com/elastic/kibana/pull/193828 and https://github.com/elastic/kibana/pull/196948 and adds an EQL Query editable component with EQL Options fields (`event_category_override`, `timestamp_field` and `tiebreaker_field`) for Three Way Diff tab's final edit side of the upgrade prebuilt rule workflow.\r\n\r\n## Details\r\n\r\nThis PR make a set of changes to make existing EQL Query bar component easily reusable and type safe when used in forms. In particular the following was done\r\n\r\n- EQL query bar was wrapped in `EqlQueryEdit` component with `UseField` inside. It helps to make it type safe avoiding issues like passing invalid types to `EqlQueryBar`. `UseField` types component properties as `Record<string, any>` so potentially any refactoring can break some functionality. For example code in Timeline passes `DataViewSpec` where `DataViewBase` is expected while these two types aren't fully compatible.\r\n- Validation was added directly to `EqlQueryEdit`. Passing field configuration to `UseField` rewrites field configuration defined in from schema. It leads to cases when validation is defined in both form schema and as a field configuration for `UseFields`. Additionally we can reduce reusing complexity by incapsulating absolutely required validation in `EqlQueryEdit` component.\r\n- Empty string `tiebreakerField` was removed in Timelines. `tiebreakerField` is part of EQL options used for EQL validation. EQL validation endpoint `/internal/search/eql` returns an error when an empty string provided for `tiebreakerField`. This problem didn't surface earlier since It looks like EQL options weren't provided correctly before this PR. Timeline EQL validation requests were sent without EQL options.\r\n\r\n## How to test\r\n\r\nThe simplest way to test is via patching installed prebuilt rules via Rule Patch API. Please follow steps below\r\n\r\n- Ensure the `prebuiltRulesCustomizationEnabled` feature flag is enabled\r\n- Run Kibana locally\r\n- Install an EQL prebuilt rule, e.g. `Potential Code Execution via Postgresql` with rule_id `2a692072-d78d-42f3-a48a-775677d79c4e`\r\n- Patch the installed rule by running a query below\r\n\r\n```bash\r\ncurl -X PATCH --user elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123' -H \"elastic-api-version: 2023-10-31\" -d '{\"rule_id\":\"2a692072-d78d-42f3-a48a-775677d79c4e\",\"version\":1,\"query\":\"process where process.name == \\\"cmd.exe\\\"\",\"language\":\"eql\",\"event_category_override\": \"test\",\"timestamp_field\": \"@timestamp\",\"tiebreaker_field\": \"tiebreaker\"}' http://localhost:5601/kbn/api/detection_engine/rules\r\n```\r\n\r\n- Open `Detection Rules (SIEM)` Page -> `Rule Updates` -> click on `Potential Code Execution via Postgresql` rule -> expand `EQL Query` to see EQL Query -> press `Edit` button\r\n\r\n## Screenshots\r\n\r\n- EQL Query in Prebuilt Rules Update workflow\r\n<img width=\"2560\" alt=\"image\" src=\"https://github.com/user-attachments/assets/59d157b2-6aca-4b21-95d0-f71a2e174df2\">\r\n\r\n- event_category_override + tiebreaker_field + timestamp_field (aka EQL options) in Prebuilt Rules Update workflow\r\n<img width=\"2552\" alt=\"image\" src=\"https://github.com/user-attachments/assets/1886d3b4-98f9-40a7-954c-2a6d4b8e925a\">\r\n\r\n- Examples of invalid EQL\r\n<img width=\"2560\" alt=\"image\" src=\"https://github.com/user-attachments/assets/d584deca-7903-45c5-9499-718552df441c\">\r\n\r\n<img width=\"2548\" alt=\"image\" src=\"https://github.com/user-attachments/assets/b734e22c-ab62-4624-85d0-e4e6dbb9d523\">","sha":"c0c803c8830c10f1df1b204a7d7b859f1f584991"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/199115","number":199115,"mergeCommit":{"message":"[Security Solution] Add EQL query editable component with EQL options fields (#199115)\n\n**Partially addresses:** https://github.com/elastic/kibana/issues/171520\r\n\r\n## Summary\r\n\r\nThis PR adds is built on top of https://github.com/elastic/kibana/pull/193828 and https://github.com/elastic/kibana/pull/196948 and adds an EQL Query editable component with EQL Options fields (`event_category_override`, `timestamp_field` and `tiebreaker_field`) for Three Way Diff tab's final edit side of the upgrade prebuilt rule workflow.\r\n\r\n## Details\r\n\r\nThis PR make a set of changes to make existing EQL Query bar component easily reusable and type safe when used in forms. In particular the following was done\r\n\r\n- EQL query bar was wrapped in `EqlQueryEdit` component with `UseField` inside. It helps to make it type safe avoiding issues like passing invalid types to `EqlQueryBar`. `UseField` types component properties as `Record<string, any>` so potentially any refactoring can break some functionality. For example code in Timeline passes `DataViewSpec` where `DataViewBase` is expected while these two types aren't fully compatible.\r\n- Validation was added directly to `EqlQueryEdit`. Passing field configuration to `UseField` rewrites field configuration defined in from schema. It leads to cases when validation is defined in both form schema and as a field configuration for `UseFields`. Additionally we can reduce reusing complexity by incapsulating absolutely required validation in `EqlQueryEdit` component.\r\n- Empty string `tiebreakerField` was removed in Timelines. `tiebreakerField` is part of EQL options used for EQL validation. EQL validation endpoint `/internal/search/eql` returns an error when an empty string provided for `tiebreakerField`. This problem didn't surface earlier since It looks like EQL options weren't provided correctly before this PR. Timeline EQL validation requests were sent without EQL options.\r\n\r\n## How to test\r\n\r\nThe simplest way to test is via patching installed prebuilt rules via Rule Patch API. Please follow steps below\r\n\r\n- Ensure the `prebuiltRulesCustomizationEnabled` feature flag is enabled\r\n- Run Kibana locally\r\n- Install an EQL prebuilt rule, e.g. `Potential Code Execution via Postgresql` with rule_id `2a692072-d78d-42f3-a48a-775677d79c4e`\r\n- Patch the installed rule by running a query below\r\n\r\n```bash\r\ncurl -X PATCH --user elastic:changeme -H 'Content-Type: application/json' -H 'kbn-xsrf: 123' -H \"elastic-api-version: 2023-10-31\" -d '{\"rule_id\":\"2a692072-d78d-42f3-a48a-775677d79c4e\",\"version\":1,\"query\":\"process where process.name == \\\"cmd.exe\\\"\",\"language\":\"eql\",\"event_category_override\": \"test\",\"timestamp_field\": \"@timestamp\",\"tiebreaker_field\": \"tiebreaker\"}' http://localhost:5601/kbn/api/detection_engine/rules\r\n```\r\n\r\n- Open `Detection Rules (SIEM)` Page -> `Rule Updates` -> click on `Potential Code Execution via Postgresql` rule -> expand `EQL Query` to see EQL Query -> press `Edit` button\r\n\r\n## Screenshots\r\n\r\n- EQL Query in Prebuilt Rules Update workflow\r\n<img width=\"2560\" alt=\"image\" src=\"https://github.com/user-attachments/assets/59d157b2-6aca-4b21-95d0-f71a2e174df2\">\r\n\r\n- event_category_override + tiebreaker_field + timestamp_field (aka EQL options) in Prebuilt Rules Update workflow\r\n<img width=\"2552\" alt=\"image\" src=\"https://github.com/user-attachments/assets/1886d3b4-98f9-40a7-954c-2a6d4b8e925a\">\r\n\r\n- Examples of invalid EQL\r\n<img width=\"2560\" alt=\"image\" src=\"https://github.com/user-attachments/assets/d584deca-7903-45c5-9499-718552df441c\">\r\n\r\n<img width=\"2548\" alt=\"image\" src=\"https://github.com/user-attachments/assets/b734e22c-ab62-4624-85d0-e4e6dbb9d523\">","sha":"c0c803c8830c10f1df1b204a7d7b859f1f584991"}},{"branch":"8.x","label":"v8.18.0","labelRegex":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT-->
This commit is contained in:
parent
54a410cb45
commit
c12646f189
73 changed files with 1131 additions and 1053 deletions
|
@ -12,3 +12,4 @@ export * from './src/axios';
|
|||
export * from './src/transform_data_to_ndjson';
|
||||
export * from './src/path_validations';
|
||||
export * from './src/esql';
|
||||
export * from './src/debounce_async/debounce_async';
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { debounceAsync } from './validators';
|
||||
import { debounceAsync } from './debounce_async';
|
||||
|
||||
jest.useFakeTimers({ legacyFakeTimers: true });
|
||||
|
||||
describe('debounceAsync', () => {
|
||||
let fn: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
fn = jest.fn().mockResolvedValueOnce('first');
|
||||
});
|
||||
|
||||
it('resolves with the underlying invocation result', async () => {
|
||||
const fn = jest.fn().mockResolvedValueOnce('first');
|
||||
|
||||
const debounced = debounceAsync(fn, 0);
|
||||
const promise = debounced();
|
||||
jest.runOnlyPendingTimers();
|
||||
|
@ -25,6 +23,8 @@ describe('debounceAsync', () => {
|
|||
});
|
||||
|
||||
it('resolves intermediate calls when the next invocation resolves', async () => {
|
||||
const fn = jest.fn().mockResolvedValueOnce('first');
|
||||
|
||||
const debounced = debounceAsync(fn, 200);
|
||||
fn.mockResolvedValueOnce('second');
|
||||
|
||||
|
@ -39,6 +39,8 @@ describe('debounceAsync', () => {
|
|||
});
|
||||
|
||||
it('debounces the function', async () => {
|
||||
const fn = jest.fn().mockResolvedValueOnce('first');
|
||||
|
||||
const debounced = debounceAsync(fn, 200);
|
||||
|
||||
debounced();
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
/**
|
||||
* Unlike lodash's debounce, which resolves intermediate calls with the most
|
||||
* recent value, this implementation waits to resolve intermediate calls until
|
||||
* the next invocation resolves.
|
||||
*
|
||||
* @param fn an async function
|
||||
*
|
||||
* @returns A debounced async function that resolves on the next invocation
|
||||
*/
|
||||
export function debounceAsync<Args extends unknown[], Result>(
|
||||
fn: (...args: Args) => Result,
|
||||
intervalMs: number
|
||||
): (...args: Args) => Promise<Awaited<Result>> {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
let resolve: (value: Awaited<Result>) => void;
|
||||
let promise = new Promise<Awaited<Result>>((_resolve) => {
|
||||
resolve = _resolve;
|
||||
});
|
||||
|
||||
return (...args) => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(async () => {
|
||||
resolve(await fn(...args));
|
||||
promise = new Promise((_resolve) => {
|
||||
resolve = _resolve;
|
||||
});
|
||||
}, intervalMs);
|
||||
|
||||
return promise;
|
||||
};
|
||||
}
|
|
@ -9,14 +9,17 @@ import { z } from '@kbn/zod';
|
|||
import {
|
||||
BuildingBlockType,
|
||||
DataViewId,
|
||||
EventCategoryOverride,
|
||||
IndexPatternArray,
|
||||
KqlQueryLanguage,
|
||||
RuleFilterArray,
|
||||
RuleNameOverride,
|
||||
RuleQuery,
|
||||
SavedQueryId,
|
||||
TiebreakerField,
|
||||
TimelineTemplateId,
|
||||
TimelineTemplateTitle,
|
||||
TimestampField,
|
||||
TimestampOverride,
|
||||
TimestampOverrideFallbackDisabled,
|
||||
} from '../../../../model/rule_schema';
|
||||
|
@ -78,6 +81,9 @@ export const RuleEqlQuery = z.object({
|
|||
query: RuleQuery,
|
||||
language: z.literal('eql'),
|
||||
filters: RuleFilterArray,
|
||||
event_category_override: EventCategoryOverride.optional(),
|
||||
timestamp_field: TimestampField.optional(),
|
||||
tiebreaker_field: TiebreakerField.optional(),
|
||||
});
|
||||
|
||||
export type RuleEsqlQuery = z.infer<typeof RuleEsqlQuery>;
|
||||
|
|
|
@ -9,7 +9,6 @@ import { z } from '@kbn/zod';
|
|||
import {
|
||||
AlertSuppression,
|
||||
AnomalyThreshold,
|
||||
EventCategoryOverride,
|
||||
HistoryWindowStart,
|
||||
InvestigationFields,
|
||||
InvestigationGuide,
|
||||
|
@ -37,8 +36,6 @@ import {
|
|||
ThreatMapping,
|
||||
Threshold,
|
||||
ThresholdAlertSuppression,
|
||||
TiebreakerField,
|
||||
TimestampField,
|
||||
} from '../../../../model/rule_schema';
|
||||
|
||||
import {
|
||||
|
@ -113,9 +110,6 @@ export const DiffableEqlFields = z.object({
|
|||
type: z.literal('eql'),
|
||||
eql_query: RuleEqlQuery, // NOTE: new field
|
||||
data_source: RuleDataSource.optional(), // NOTE: new field
|
||||
event_category_override: EventCategoryOverride.optional(),
|
||||
timestamp_field: TimestampField.optional(),
|
||||
tiebreaker_field: TiebreakerField.optional(),
|
||||
alert_suppression: AlertSuppression.optional(),
|
||||
});
|
||||
|
||||
|
|
|
@ -175,11 +175,15 @@ const extractDiffableEqlFieldsFromRuleObject = (
|
|||
): RequiredOptional<DiffableEqlFields> => {
|
||||
return {
|
||||
type: rule.type,
|
||||
eql_query: extractRuleEqlQuery(rule.query, rule.language, rule.filters),
|
||||
eql_query: extractRuleEqlQuery({
|
||||
query: rule.query,
|
||||
language: rule.language,
|
||||
filters: rule.filters,
|
||||
eventCategoryOverride: rule.event_category_override,
|
||||
timestampField: rule.timestamp_field,
|
||||
tiebreakerField: rule.tiebreaker_field,
|
||||
}),
|
||||
data_source: extractRuleDataSource(rule.index, rule.data_view_id),
|
||||
event_category_override: rule.event_category_override,
|
||||
timestamp_field: rule.timestamp_field,
|
||||
tiebreaker_field: rule.tiebreaker_field,
|
||||
alert_suppression: rule.alert_suppression,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,9 +8,12 @@
|
|||
import type {
|
||||
EqlQueryLanguage,
|
||||
EsqlQueryLanguage,
|
||||
EventCategoryOverride,
|
||||
KqlQueryLanguage,
|
||||
RuleFilterArray,
|
||||
RuleQuery,
|
||||
TiebreakerField,
|
||||
TimestampField,
|
||||
} from '../../../api/detection_engine/model/rule_schema';
|
||||
import type {
|
||||
InlineKqlQuery,
|
||||
|
@ -49,15 +52,23 @@ export const extractInlineKqlQuery = (
|
|||
};
|
||||
};
|
||||
|
||||
export const extractRuleEqlQuery = (
|
||||
query: RuleQuery,
|
||||
language: EqlQueryLanguage,
|
||||
filters: RuleFilterArray | undefined
|
||||
): RuleEqlQuery => {
|
||||
interface ExtractRuleEqlQueryParams {
|
||||
query: RuleQuery;
|
||||
language: EqlQueryLanguage;
|
||||
filters: RuleFilterArray | undefined;
|
||||
eventCategoryOverride: EventCategoryOverride | undefined;
|
||||
timestampField: TimestampField | undefined;
|
||||
tiebreakerField: TiebreakerField | undefined;
|
||||
}
|
||||
|
||||
export const extractRuleEqlQuery = (params: ExtractRuleEqlQueryParams): RuleEqlQuery => {
|
||||
return {
|
||||
query,
|
||||
language,
|
||||
filters: filters ?? [],
|
||||
query: params.query,
|
||||
language: params.language,
|
||||
filters: params.filters ?? [],
|
||||
event_category_override: params.eventCategoryOverride,
|
||||
timestamp_field: params.timestampField,
|
||||
tiebreaker_field: params.tiebreakerField,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
export type {
|
||||
TimelineEqlResponse,
|
||||
EqlOptionsData,
|
||||
EqlOptionsSelected,
|
||||
EqlFieldsComboBoxOptions,
|
||||
EqlOptions,
|
||||
FieldsEqlOptions,
|
||||
} from '@kbn/timelines-plugin/common';
|
||||
|
|
|
@ -34,7 +34,7 @@ const triggerValidateEql = () => {
|
|||
query: 'any where true',
|
||||
signal,
|
||||
runtimeMappings: undefined,
|
||||
options: undefined,
|
||||
eqlOptions: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import type { EqlSearchStrategyRequest, EqlSearchStrategyResponse } from '@kbn/d
|
|||
import { EQL_SEARCH_STRATEGY } from '@kbn/data-plugin/common';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
||||
import type { EqlOptionsSelected } from '../../../../common/search_strategy';
|
||||
import type { EqlOptions } from '../../../../common/search_strategy';
|
||||
import {
|
||||
getValidationErrors,
|
||||
isErrorResponse,
|
||||
|
@ -31,9 +31,9 @@ interface Params {
|
|||
dataViewTitle: string;
|
||||
query: string;
|
||||
data: DataPublicPluginStart;
|
||||
signal: AbortSignal;
|
||||
runtimeMappings: estypes.MappingRuntimeFields | undefined;
|
||||
options: Omit<EqlOptionsSelected, 'query' | 'size'> | undefined;
|
||||
eqlOptions: Omit<EqlOptions, 'query' | 'size'> | undefined;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface EqlResponseError {
|
||||
|
@ -51,9 +51,9 @@ export const validateEql = async ({
|
|||
data,
|
||||
dataViewTitle,
|
||||
query,
|
||||
signal,
|
||||
runtimeMappings,
|
||||
options,
|
||||
eqlOptions,
|
||||
signal,
|
||||
}: Params): Promise<ValidateEqlResponse> => {
|
||||
try {
|
||||
const { rawResponse: response } = await firstValueFrom(
|
||||
|
@ -62,9 +62,12 @@ export const validateEql = async ({
|
|||
params: {
|
||||
index: dataViewTitle,
|
||||
body: { query, runtime_mappings: runtimeMappings, size: 0 },
|
||||
timestamp_field: options?.timestampField,
|
||||
tiebreaker_field: options?.tiebreakerField || undefined,
|
||||
event_category_field: options?.eventCategoryField,
|
||||
// Prevent passing empty string values
|
||||
timestamp_field: eqlOptions?.timestampField ? eqlOptions.timestampField : undefined,
|
||||
tiebreaker_field: eqlOptions?.tiebreakerField ? eqlOptions.tiebreakerField : undefined,
|
||||
event_category_field: eqlOptions?.eventCategoryField
|
||||
? eqlOptions.eventCategoryField
|
||||
: undefined,
|
||||
},
|
||||
options: { ignore: [400] },
|
||||
},
|
||||
|
@ -79,19 +82,23 @@ export const validateEql = async ({
|
|||
valid: false,
|
||||
error: { code: EQL_ERROR_CODES.INVALID_SYNTAX, messages: getValidationErrors(response) },
|
||||
};
|
||||
} else if (isVerificationErrorResponse(response) || isMappingErrorResponse(response)) {
|
||||
}
|
||||
|
||||
if (isVerificationErrorResponse(response) || isMappingErrorResponse(response)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: { code: EQL_ERROR_CODES.INVALID_EQL, messages: getValidationErrors(response) },
|
||||
};
|
||||
} else if (isErrorResponse(response)) {
|
||||
}
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: { code: EQL_ERROR_CODES.FAILED_REQUEST, error: new Error(JSON.stringify(response)) },
|
||||
};
|
||||
} else {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.startsWith('index_not_found_exception')) {
|
||||
return {
|
||||
|
@ -99,6 +106,7 @@ export const validateEql = async ({
|
|||
error: { code: EQL_ERROR_CODES.MISSING_DATA_SOURCE, messages: [error.message] },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
error: {
|
||||
|
|
|
@ -349,7 +349,6 @@ export const mockGlobalState: State = {
|
|||
description: '',
|
||||
eqlOptions: {
|
||||
eventCategoryField: 'event.category',
|
||||
tiebreakerField: '',
|
||||
timestampField: '@timestamp',
|
||||
},
|
||||
eventIdToNoteIds: { '1': ['1'] },
|
||||
|
|
|
@ -2051,7 +2051,6 @@ export const defaultTimelineProps: CreateTimelineProps = {
|
|||
eventCategoryField: 'event.category',
|
||||
query: '',
|
||||
size: 100,
|
||||
tiebreakerField: '',
|
||||
timestampField: '@timestamp',
|
||||
},
|
||||
eventIdToNoteIds: {},
|
||||
|
|
|
@ -8,9 +8,10 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, screen, fireEvent, within } from '@testing-library/react';
|
||||
|
||||
import type { SecuritySolutionDataViewBase } from '../../../../common/types';
|
||||
import { mockIndexPattern, TestProviders, useFormFieldMock } from '../../../../common/mock';
|
||||
import { mockQueryBar } from '../../../rule_management_ui/components/rules_table/__mocks__/mock';
|
||||
import { selectEuiComboBoxOption } from '../../../../common/test/eui/combobox';
|
||||
import type { EqlQueryBarProps } from './eql_query_bar';
|
||||
import { EqlQueryBar } from './eql_query_bar';
|
||||
import { getEqlValidationError } from './validators.mock';
|
||||
|
@ -115,36 +116,82 @@ describe('EqlQueryBar', () => {
|
|||
});
|
||||
|
||||
describe('EQL options interaction', () => {
|
||||
const mockOptionsData = {
|
||||
keywordFields: [],
|
||||
dateFields: [{ label: 'timestamp', value: 'timestamp' }],
|
||||
nonDateFields: [],
|
||||
const mockIndexPatternWithEqlOptionsFields: SecuritySolutionDataViewBase = {
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
searchable: true,
|
||||
type: 'keyword',
|
||||
esTypes: ['keyword'],
|
||||
aggregatable: true,
|
||||
},
|
||||
{
|
||||
name: 'timestamp',
|
||||
searchable: true,
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
},
|
||||
{
|
||||
name: 'tiebreaker',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
aggregatable: true,
|
||||
},
|
||||
],
|
||||
title: 'test-*',
|
||||
};
|
||||
|
||||
it('invokes onOptionsChange when the EQL options change', () => {
|
||||
const onOptionsChangeMock = jest.fn();
|
||||
it('updates EQL options', async () => {
|
||||
let eqlOptions = {};
|
||||
|
||||
const { getByTestId, getByText } = render(
|
||||
const mockEqlOptionsField = useFormFieldMock({
|
||||
value: {},
|
||||
setValue: (updater) => {
|
||||
if (typeof updater === 'function') {
|
||||
eqlOptions = updater(eqlOptions);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<EqlQueryBar
|
||||
dataTestSubj="myQueryBar"
|
||||
field={mockField}
|
||||
eqlOptionsField={mockEqlOptionsField}
|
||||
isLoading={false}
|
||||
optionsData={mockOptionsData}
|
||||
indexPattern={mockIndexPattern}
|
||||
onOptionsChange={onOptionsChangeMock}
|
||||
indexPattern={mockIndexPatternWithEqlOptionsFields}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
// open options popover
|
||||
fireEvent.click(getByTestId('eql-settings-trigger'));
|
||||
// display combobox options
|
||||
within(getByTestId(`eql-timestamp-field`)).getByRole('combobox').focus();
|
||||
// select timestamp
|
||||
getByText('timestamp').click();
|
||||
|
||||
expect(onOptionsChangeMock).toHaveBeenCalledWith('timestampField', 'timestamp');
|
||||
await selectEuiComboBoxOption({
|
||||
comboBoxToggleButton: within(getByTestId('eql-event-category-field')).getByRole('combobox'),
|
||||
optionText: 'category',
|
||||
});
|
||||
|
||||
expect(eqlOptions).toEqual({ eventCategoryField: 'category' });
|
||||
|
||||
await selectEuiComboBoxOption({
|
||||
comboBoxToggleButton: within(getByTestId('eql-tiebreaker-field')).getByRole('combobox'),
|
||||
optionText: 'tiebreaker',
|
||||
});
|
||||
|
||||
expect(eqlOptions).toEqual({ eventCategoryField: 'category', tiebreakerField: 'tiebreaker' });
|
||||
|
||||
await selectEuiComboBoxOption({
|
||||
comboBoxToggleButton: within(getByTestId('eql-timestamp-field')).getByRole('combobox'),
|
||||
optionText: 'timestamp',
|
||||
});
|
||||
|
||||
expect(eqlOptions).toEqual({
|
||||
eventCategoryField: 'category',
|
||||
tiebreakerField: 'tiebreaker',
|
||||
timestampField: 'timestamp',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -17,16 +17,13 @@ import { FilterManager } from '@kbn/data-plugin/public';
|
|||
import type { FieldHook } from '../../../../shared_imports';
|
||||
import { FilterBar } from '../../../../common/components/filter_bar';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import type { DefineStepRule } from '../../../../detections/pages/detection_engine/rules/types';
|
||||
import * as i18n from './translations';
|
||||
import type { EqlOptions } from '../../../../../common/search_strategy';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import type { FieldValueQueryBar } from '../../../rule_creation_ui/components/query_bar';
|
||||
import type { EqlQueryBarFooterProps } from './footer';
|
||||
import { EqlQueryBarFooter } from './footer';
|
||||
import { getValidationResults } from './validators';
|
||||
import type {
|
||||
EqlOptionsData,
|
||||
EqlOptionsSelected,
|
||||
FieldsEqlOptions,
|
||||
} from '../../../../../common/search_strategy';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const TextArea = styled(EuiTextArea)`
|
||||
display: block;
|
||||
|
@ -60,32 +57,28 @@ const StyledFormRow = styled(EuiFormRow)`
|
|||
|
||||
export interface EqlQueryBarProps {
|
||||
dataTestSubj: string;
|
||||
field: FieldHook<DefineStepRule['queryBar']>;
|
||||
isLoading: boolean;
|
||||
field: FieldHook<FieldValueQueryBar>;
|
||||
eqlOptionsField?: FieldHook<EqlOptions>;
|
||||
isLoading?: boolean;
|
||||
indexPattern: DataViewBase;
|
||||
showFilterBar?: boolean;
|
||||
idAria?: string;
|
||||
optionsData?: EqlOptionsData;
|
||||
optionsSelected?: EqlOptionsSelected;
|
||||
isSizeOptionDisabled?: boolean;
|
||||
onOptionsChange?: (field: FieldsEqlOptions, newValue: string | undefined) => void;
|
||||
onValidityChange?: (arg: boolean) => void;
|
||||
onValiditingChange?: (arg: boolean) => void;
|
||||
onValidatingChange?: (arg: boolean) => void;
|
||||
}
|
||||
|
||||
export const EqlQueryBar: FC<EqlQueryBarProps> = ({
|
||||
dataTestSubj,
|
||||
field,
|
||||
eqlOptionsField,
|
||||
isLoading = false,
|
||||
indexPattern,
|
||||
showFilterBar,
|
||||
idAria,
|
||||
optionsData,
|
||||
optionsSelected,
|
||||
isSizeOptionDisabled,
|
||||
onOptionsChange,
|
||||
onValidityChange,
|
||||
onValiditingChange,
|
||||
onValidatingChange,
|
||||
}) => {
|
||||
const { addError } = useAppToasts();
|
||||
const [errorMessages, setErrorMessages] = useState<string[]>([]);
|
||||
|
@ -115,10 +108,10 @@ export const EqlQueryBar: FC<EqlQueryBarProps> = ({
|
|||
}, [error, addError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onValiditingChange) {
|
||||
onValiditingChange(isValidating);
|
||||
if (onValidatingChange) {
|
||||
onValidatingChange(isValidating);
|
||||
}
|
||||
}, [isValidating, onValiditingChange]);
|
||||
}, [isValidating, onValidatingChange]);
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
|
@ -156,8 +149,8 @@ export const EqlQueryBar: FC<EqlQueryBarProps> = ({
|
|||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newQuery = e.target.value;
|
||||
if (onValiditingChange) {
|
||||
onValiditingChange(true);
|
||||
if (onValidatingChange) {
|
||||
onValidatingChange(true);
|
||||
}
|
||||
setErrorMessages([]);
|
||||
setFieldValue({
|
||||
|
@ -169,7 +162,19 @@ export const EqlQueryBar: FC<EqlQueryBarProps> = ({
|
|||
saved_id: null,
|
||||
});
|
||||
},
|
||||
[fieldValue, setFieldValue, onValiditingChange]
|
||||
[fieldValue, setFieldValue, onValidatingChange]
|
||||
);
|
||||
|
||||
const handleEqlOptionsChange = useCallback<
|
||||
NonNullable<EqlQueryBarFooterProps['onEqlOptionsChange']>
|
||||
>(
|
||||
(eqlOptionsFieldName, value) => {
|
||||
eqlOptionsField?.setValue((prevEqlOptions) => ({
|
||||
...prevEqlOptions,
|
||||
[eqlOptionsFieldName]: value,
|
||||
}));
|
||||
},
|
||||
[eqlOptionsField]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -195,9 +200,9 @@ export const EqlQueryBar: FC<EqlQueryBarProps> = ({
|
|||
errors={errorMessages}
|
||||
isLoading={isValidating}
|
||||
isSizeOptionDisabled={isSizeOptionDisabled}
|
||||
optionsData={optionsData}
|
||||
optionsSelected={optionsSelected}
|
||||
onOptionsChange={onOptionsChange}
|
||||
dataView={indexPattern}
|
||||
eqlOptions={eqlOptionsField?.value}
|
||||
onEqlOptionsChange={handleEqlOptionsChange}
|
||||
/>
|
||||
{showFilterBar && (
|
||||
<>
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import type { DataViewBase } from '@kbn/es-query';
|
||||
import { debounceAsync } from '@kbn/securitysolution-utils';
|
||||
import type { FormData, FieldConfig, ValidationFuncArg } from '../../../../shared_imports';
|
||||
import { UseMultiFields } from '../../../../shared_imports';
|
||||
import type { EqlFieldsComboBoxOptions, EqlOptions } from '../../../../../common/search_strategy';
|
||||
import { queryRequiredValidatorFactory } from '../../../rule_creation_ui/validators/query_required_validator_factory';
|
||||
import { eqlQueryValidatorFactory } from './eql_query_validator_factory';
|
||||
import { EqlQueryBar } from './eql_query_bar';
|
||||
import * as i18n from './translations';
|
||||
import type { FieldValueQueryBar } from '../../../rule_creation_ui/components/query_bar';
|
||||
|
||||
interface EqlQueryEditProps {
|
||||
path: string;
|
||||
eqlOptionsPath: string;
|
||||
fieldsToValidateOnChange?: string | string[];
|
||||
eqlFieldsComboBoxOptions?: EqlFieldsComboBoxOptions;
|
||||
showEqlSizeOption?: boolean;
|
||||
showFilterBar?: boolean;
|
||||
dataView: DataViewBase;
|
||||
required?: boolean;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
// This is a temporal solution for Prebuilt Customization workflow
|
||||
skipEqlValidation?: boolean;
|
||||
onValidityChange?: (arg: boolean) => void;
|
||||
}
|
||||
|
||||
export function EqlQueryEdit({
|
||||
path,
|
||||
eqlOptionsPath,
|
||||
fieldsToValidateOnChange,
|
||||
showEqlSizeOption = false,
|
||||
showFilterBar = false,
|
||||
dataView,
|
||||
required,
|
||||
loading,
|
||||
disabled,
|
||||
skipEqlValidation,
|
||||
onValidityChange,
|
||||
}: EqlQueryEditProps): JSX.Element {
|
||||
const componentProps = useMemo(
|
||||
() => ({
|
||||
isSizeOptionDisabled: !showEqlSizeOption,
|
||||
isDisabled: disabled,
|
||||
isLoading: loading,
|
||||
indexPattern: dataView,
|
||||
showFilterBar,
|
||||
idAria: 'ruleEqlQueryBar',
|
||||
dataTestSubj: 'ruleEqlQueryBar',
|
||||
onValidityChange,
|
||||
}),
|
||||
[showEqlSizeOption, showFilterBar, onValidityChange, dataView, loading, disabled]
|
||||
);
|
||||
const fieldConfig: FieldConfig<FieldValueQueryBar> = useMemo(
|
||||
() => ({
|
||||
label: i18n.EQL_QUERY_BAR_LABEL,
|
||||
fieldsToValidateOnChange: fieldsToValidateOnChange
|
||||
? [path, fieldsToValidateOnChange].flat()
|
||||
: undefined,
|
||||
validations: [
|
||||
...(required
|
||||
? [
|
||||
{
|
||||
validator: queryRequiredValidatorFactory('eql'),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!skipEqlValidation
|
||||
? [
|
||||
{
|
||||
validator: debounceAsync(
|
||||
(data: ValidationFuncArg<FormData, FieldValueQueryBar>) => {
|
||||
const { formData } = data;
|
||||
const eqlOptions =
|
||||
eqlOptionsPath && formData[eqlOptionsPath] ? formData[eqlOptionsPath] : {};
|
||||
|
||||
return eqlQueryValidatorFactory(
|
||||
dataView.id
|
||||
? {
|
||||
dataViewId: dataView.id,
|
||||
eqlOptions,
|
||||
}
|
||||
: {
|
||||
indexPatterns: dataView.title.split(','),
|
||||
eqlOptions,
|
||||
}
|
||||
)(data);
|
||||
},
|
||||
300
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
[
|
||||
skipEqlValidation,
|
||||
eqlOptionsPath,
|
||||
required,
|
||||
dataView.id,
|
||||
dataView.title,
|
||||
path,
|
||||
fieldsToValidateOnChange,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<UseMultiFields<{
|
||||
eqlQuery: FieldValueQueryBar;
|
||||
eqlOptions: EqlOptions;
|
||||
}>
|
||||
fields={{
|
||||
eqlQuery: {
|
||||
path,
|
||||
config: fieldConfig,
|
||||
},
|
||||
eqlOptions: {
|
||||
path: eqlOptionsPath,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{({ eqlQuery, eqlOptions }) => (
|
||||
<EqlQueryBar field={eqlQuery} eqlOptionsField={eqlOptions} {...componentProps} />
|
||||
)}
|
||||
</UseMultiFields>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { isEmpty } from 'lodash';
|
||||
import type { FormData, ValidationError, ValidationFunc } from '../../../../shared_imports';
|
||||
import { KibanaServices } from '../../../../common/lib/kibana';
|
||||
import type { EqlOptions } from '../../../../../common/search_strategy';
|
||||
import type { FieldValueQueryBar } from '../../../rule_creation_ui/components/query_bar';
|
||||
import type { EqlResponseError } from '../../../../common/hooks/eql/api';
|
||||
import { EQL_ERROR_CODES, validateEql } from '../../../../common/hooks/eql/api';
|
||||
import { EQL_VALIDATION_REQUEST_ERROR } from './translations';
|
||||
|
||||
type EqlQueryValidatorFactoryParams =
|
||||
| {
|
||||
indexPatterns: string[];
|
||||
dataViewId?: never;
|
||||
eqlOptions: EqlOptions;
|
||||
}
|
||||
| {
|
||||
indexPatterns?: never;
|
||||
dataViewId: string;
|
||||
eqlOptions: EqlOptions;
|
||||
};
|
||||
|
||||
export function eqlQueryValidatorFactory({
|
||||
indexPatterns,
|
||||
dataViewId,
|
||||
eqlOptions,
|
||||
}: EqlQueryValidatorFactoryParams): ValidationFunc<FormData, string, FieldValueQueryBar> {
|
||||
return async (...args) => {
|
||||
const [{ value }] = args;
|
||||
|
||||
if (isEmpty(value.query.query)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = KibanaServices.get();
|
||||
const dataView = isDataViewIdValid(dataViewId)
|
||||
? await data.dataViews.get(dataViewId)
|
||||
: undefined;
|
||||
|
||||
const dataViewTitle = dataView?.getIndexPattern() ?? indexPatterns?.join(',') ?? '';
|
||||
const runtimeMappings = dataView?.getRuntimeMappings() ?? {};
|
||||
|
||||
const response = await validateEql({
|
||||
data,
|
||||
query: value.query.query as string,
|
||||
dataViewTitle,
|
||||
runtimeMappings,
|
||||
eqlOptions,
|
||||
});
|
||||
|
||||
if (response?.valid === false && response.error) {
|
||||
return transformEqlResponseErrorToValidationError(response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
code: EQL_ERROR_CODES.FAILED_REQUEST,
|
||||
message: EQL_VALIDATION_REQUEST_ERROR,
|
||||
error,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function transformEqlResponseErrorToValidationError(
|
||||
responseError: EqlResponseError
|
||||
): ValidationError<EQL_ERROR_CODES> {
|
||||
if (responseError.error) {
|
||||
return {
|
||||
code: EQL_ERROR_CODES.FAILED_REQUEST,
|
||||
message: EQL_VALIDATION_REQUEST_ERROR,
|
||||
error: responseError.error,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: responseError.code,
|
||||
message: '',
|
||||
messages: responseError.messages,
|
||||
};
|
||||
}
|
||||
|
||||
function isDataViewIdValid(dataViewId: unknown): dataViewId is string {
|
||||
return typeof dataViewId === 'string' && dataViewId !== '';
|
||||
}
|
|
@ -32,7 +32,11 @@ describe('EQL footer', () => {
|
|||
it('EQL settings button is enable when popover is NOT open', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<EqlQueryBarFooter errors={[]} onOptionsChange={jest.fn()} />
|
||||
<EqlQueryBarFooter
|
||||
errors={[]}
|
||||
dataView={{ title: '', fields: [] }}
|
||||
onEqlOptionsChange={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -44,7 +48,11 @@ describe('EQL footer', () => {
|
|||
it('disable EQL settings button when popover is open', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<EqlQueryBarFooter errors={[]} onOptionsChange={jest.fn()} />
|
||||
<EqlQueryBarFooter
|
||||
errors={[]}
|
||||
dataView={{ title: '', fields: [] }}
|
||||
onEqlOptionsChange={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find(`[data-test-subj="eql-settings-trigger"]`).first().simulate('click');
|
|
@ -20,28 +20,27 @@ import {
|
|||
import type { FC } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { DataViewBase } from '@kbn/es-query';
|
||||
import type { DebouncedFunc } from 'lodash';
|
||||
import { debounce } from 'lodash';
|
||||
import type {
|
||||
EqlOptionsData,
|
||||
EqlOptionsSelected,
|
||||
FieldsEqlOptions,
|
||||
} from '../../../../../common/search_strategy';
|
||||
import { debounce, isEmpty } from 'lodash';
|
||||
import type { EqlOptions } from '../../../../../common/search_strategy';
|
||||
import * as i18n from './translations';
|
||||
import { ErrorsPopover } from './errors_popover';
|
||||
import { EqlOverviewLink } from './eql_overview_link';
|
||||
|
||||
export interface Props {
|
||||
export interface EqlQueryBarFooterProps {
|
||||
errors: string[];
|
||||
isLoading?: boolean;
|
||||
isSizeOptionDisabled?: boolean;
|
||||
optionsData?: EqlOptionsData;
|
||||
optionsSelected?: EqlOptionsSelected;
|
||||
onOptionsChange?: (field: FieldsEqlOptions, newValue: string | undefined) => void;
|
||||
dataView: DataViewBase;
|
||||
eqlOptions?: EqlOptions;
|
||||
onEqlOptionsChange?: <Field extends keyof EqlOptions>(
|
||||
field: Field,
|
||||
newValue: EqlOptions[Field]
|
||||
) => void;
|
||||
}
|
||||
|
||||
type SizeVoidFunc = (newSize: string) => void;
|
||||
type SizeVoidFunc = (newSize: number) => void;
|
||||
|
||||
const Container = styled(EuiFlexGroup)`
|
||||
border-radius: 0;
|
||||
|
@ -69,18 +68,40 @@ const Spinner = styled(EuiLoadingSpinner)`
|
|||
|
||||
const singleSelection = { asPlainText: true };
|
||||
|
||||
export const EqlQueryBarFooter: FC<Props> = ({
|
||||
export const EqlQueryBarFooter: FC<EqlQueryBarFooterProps> = ({
|
||||
errors,
|
||||
isLoading,
|
||||
isSizeOptionDisabled,
|
||||
optionsData,
|
||||
optionsSelected,
|
||||
onOptionsChange,
|
||||
dataView,
|
||||
eqlOptions,
|
||||
onEqlOptionsChange,
|
||||
}) => {
|
||||
const [openEqlSettings, setIsOpenEqlSettings] = useState(false);
|
||||
const [localSize, setLocalSize] = useState<string | number>(optionsSelected?.size ?? 100);
|
||||
const [localSize, setLocalSize] = useState<number>(eqlOptions?.size ?? 100);
|
||||
const debounceSize = useRef<DebouncedFunc<SizeVoidFunc>>();
|
||||
|
||||
const { keywordFields, nonDateFields, dateFields } = useMemo(
|
||||
() =>
|
||||
isEmpty(dataView?.fields)
|
||||
? {
|
||||
keywordFields: [],
|
||||
dateFields: [],
|
||||
nonDateFields: [],
|
||||
}
|
||||
: {
|
||||
keywordFields: dataView.fields
|
||||
.filter((f) => f.esTypes?.includes('keyword'))
|
||||
.map((f) => ({ label: f.name })),
|
||||
dateFields: dataView.fields
|
||||
.filter((f) => f.type === 'date')
|
||||
.map((f) => ({ label: f.name })),
|
||||
nonDateFields: dataView.fields
|
||||
.filter((f) => f.type !== 'date')
|
||||
.map((f) => ({ label: f.name })),
|
||||
},
|
||||
[dataView]
|
||||
);
|
||||
|
||||
const openEqlSettingsHandler = useCallback(() => {
|
||||
setIsOpenEqlSettings(true);
|
||||
}, []);
|
||||
|
@ -90,74 +111,70 @@ export const EqlQueryBarFooter: FC<Props> = ({
|
|||
|
||||
const handleEventCategoryField = useCallback(
|
||||
(opt: EuiComboBoxOptionOption[]) => {
|
||||
if (onOptionsChange) {
|
||||
if (onEqlOptionsChange) {
|
||||
if (opt.length > 0) {
|
||||
onOptionsChange('eventCategoryField', opt[0].label);
|
||||
onEqlOptionsChange('eventCategoryField', opt[0].label);
|
||||
} else {
|
||||
onOptionsChange('eventCategoryField', undefined);
|
||||
onEqlOptionsChange('eventCategoryField', undefined);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onOptionsChange]
|
||||
[onEqlOptionsChange]
|
||||
);
|
||||
const handleTiebreakerField = useCallback(
|
||||
(opt: EuiComboBoxOptionOption[]) => {
|
||||
if (onOptionsChange) {
|
||||
if (onEqlOptionsChange) {
|
||||
if (opt.length > 0) {
|
||||
onOptionsChange('tiebreakerField', opt[0].label);
|
||||
onEqlOptionsChange('tiebreakerField', opt[0].label);
|
||||
} else {
|
||||
onOptionsChange('tiebreakerField', undefined);
|
||||
onEqlOptionsChange('tiebreakerField', undefined);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onOptionsChange]
|
||||
[onEqlOptionsChange]
|
||||
);
|
||||
const handleTimestampField = useCallback(
|
||||
(opt: EuiComboBoxOptionOption[]) => {
|
||||
if (onOptionsChange) {
|
||||
if (onEqlOptionsChange) {
|
||||
if (opt.length > 0) {
|
||||
onOptionsChange('timestampField', opt[0].label);
|
||||
onEqlOptionsChange('timestampField', opt[0].label);
|
||||
} else {
|
||||
onOptionsChange('timestampField', undefined);
|
||||
onEqlOptionsChange('timestampField', undefined);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onOptionsChange]
|
||||
[onEqlOptionsChange]
|
||||
);
|
||||
const handleSizeField = useCallback<NonNullable<EuiFieldNumberProps['onChange']>>(
|
||||
(evt) => {
|
||||
if (onOptionsChange) {
|
||||
if (onEqlOptionsChange) {
|
||||
setLocalSize(evt?.target?.valueAsNumber);
|
||||
if (debounceSize.current?.cancel) {
|
||||
debounceSize.current?.cancel();
|
||||
}
|
||||
debounceSize.current = debounce((newSize) => onOptionsChange('size', newSize), 800);
|
||||
debounceSize.current(evt?.target?.value);
|
||||
debounceSize.current = debounce((newSize) => onEqlOptionsChange('size', newSize), 800);
|
||||
debounceSize.current(evt?.target?.valueAsNumber);
|
||||
}
|
||||
},
|
||||
[onOptionsChange]
|
||||
[onEqlOptionsChange]
|
||||
);
|
||||
|
||||
const eventCategoryField = useMemo(
|
||||
() =>
|
||||
optionsSelected?.eventCategoryField != null
|
||||
? [{ label: optionsSelected?.eventCategoryField }]
|
||||
eqlOptions?.eventCategoryField != null
|
||||
? [{ label: eqlOptions?.eventCategoryField }]
|
||||
: undefined,
|
||||
[optionsSelected?.eventCategoryField]
|
||||
[eqlOptions?.eventCategoryField]
|
||||
);
|
||||
const tiebreakerField = useMemo(
|
||||
() =>
|
||||
optionsSelected?.tiebreakerField != null
|
||||
? [{ label: optionsSelected?.tiebreakerField }]
|
||||
: undefined,
|
||||
[optionsSelected?.tiebreakerField]
|
||||
eqlOptions?.tiebreakerField != null ? [{ label: eqlOptions?.tiebreakerField }] : undefined,
|
||||
[eqlOptions?.tiebreakerField]
|
||||
);
|
||||
const timestampField = useMemo(
|
||||
() =>
|
||||
optionsSelected?.timestampField != null
|
||||
? [{ label: optionsSelected?.timestampField }]
|
||||
: undefined,
|
||||
[optionsSelected?.timestampField]
|
||||
eqlOptions?.timestampField != null ? [{ label: eqlOptions?.timestampField }] : undefined,
|
||||
[eqlOptions?.timestampField]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -183,13 +200,13 @@ export const EqlQueryBarFooter: FC<Props> = ({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize={'none'} alignItems="center" responsive={false}>
|
||||
{!onOptionsChange && (
|
||||
{!onEqlOptionsChange && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EqlOverviewLink />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{onOptionsChange && (
|
||||
{onEqlOptionsChange && (
|
||||
<>
|
||||
<FlexItemWithMarginRight grow={false}>
|
||||
<EqlOverviewLink />
|
||||
|
@ -232,7 +249,7 @@ export const EqlQueryBarFooter: FC<Props> = ({
|
|||
helpText={i18n.EQL_OPTIONS_EVENT_CATEGORY_FIELD_HELPER}
|
||||
>
|
||||
<EuiComboBox
|
||||
options={optionsData?.keywordFields}
|
||||
options={keywordFields}
|
||||
selectedOptions={eventCategoryField}
|
||||
singleSelection={singleSelection}
|
||||
onChange={handleEventCategoryField}
|
||||
|
@ -244,7 +261,7 @@ export const EqlQueryBarFooter: FC<Props> = ({
|
|||
helpText={i18n.EQL_OPTIONS_EVENT_TIEBREAKER_FIELD_HELPER}
|
||||
>
|
||||
<EuiComboBox
|
||||
options={optionsData?.nonDateFields}
|
||||
options={nonDateFields}
|
||||
selectedOptions={tiebreakerField}
|
||||
singleSelection={singleSelection}
|
||||
onChange={handleTiebreakerField}
|
||||
|
@ -256,7 +273,7 @@ export const EqlQueryBarFooter: FC<Props> = ({
|
|||
helpText={i18n.EQL_OPTIONS_EVENT_TIMESTAMP_FIELD_HELPER}
|
||||
>
|
||||
<EuiComboBox
|
||||
options={optionsData?.dateFields}
|
||||
options={dateFields}
|
||||
selectedOptions={timestampField}
|
||||
singleSelection={singleSelection}
|
||||
onChange={handleTimestampField}
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { EqlQueryBar } from './eql_query_bar';
|
||||
export * from './eql_query_edit';
|
|
@ -7,6 +7,13 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const EQL_QUERY_BAR_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.EqlQueryBarLabel',
|
||||
{
|
||||
defaultMessage: 'EQL query',
|
||||
}
|
||||
);
|
||||
|
||||
export const EQL_VALIDATION_REQUEST_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.eqlValidation.requestError',
|
||||
{
|
|
@ -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 type { FieldHook } from '../../../../shared_imports';
|
||||
import { EQL_ERROR_CODES } from '../../../../common/hooks/eql/api';
|
||||
|
||||
export const getValidationResults = <T = unknown>(
|
||||
field: FieldHook<T>
|
||||
): { isValid: boolean; message: string; messages?: string[]; error?: Error } => {
|
||||
const hasErrors = field.errors.length > 0;
|
||||
const isValid = !field.isChangingValue && !hasErrors;
|
||||
|
||||
if (hasErrors) {
|
||||
const [error] = field.errors;
|
||||
const message = error.message;
|
||||
|
||||
if (error.code === EQL_ERROR_CODES.FAILED_REQUEST) {
|
||||
return { isValid, message, error: error.error };
|
||||
} else {
|
||||
return { isValid, message, messages: error.messages };
|
||||
}
|
||||
} else {
|
||||
return { isValid, message: '' };
|
||||
}
|
||||
};
|
|
@ -32,7 +32,7 @@ import type {
|
|||
} from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { AlertSuppressionMissingFieldsStrategyEnum } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { MATCHES, AND, OR } from '../../../../common/components/threat_match/translations';
|
||||
import type { EqlOptionsSelected } from '../../../../../common/search_strategy';
|
||||
import type { EqlOptions } from '../../../../../common/search_strategy';
|
||||
import { assertUnreachable } from '../../../../../common/utility_types';
|
||||
import * as i18nSeverity from '../severity_mapping/translations';
|
||||
import * as i18nRiskScore from '../risk_score_mapping/translations';
|
||||
|
@ -147,7 +147,7 @@ export const buildQueryBarDescription = ({
|
|||
return items;
|
||||
};
|
||||
|
||||
export const buildEqlOptionsDescription = (eqlOptions: EqlOptionsSelected): ListItems[] => {
|
||||
export const buildEqlOptionsDescription = (eqlOptions: EqlOptions): ListItems[] => {
|
||||
let items: ListItems[] = [];
|
||||
if (!isEmpty(eqlOptions.eventCategoryField)) {
|
||||
items = [
|
||||
|
|
|
@ -19,7 +19,7 @@ import type {
|
|||
} from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { buildRelatedIntegrationsDescription } from '../../../../detections/components/rules/related_integrations/integrations_description';
|
||||
import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations';
|
||||
import type { EqlOptionsSelected } from '../../../../../common/search_strategy';
|
||||
import type { EqlOptions } from '../../../../../common/search_strategy';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import type {
|
||||
AboutStepRiskScore,
|
||||
|
@ -275,7 +275,7 @@ export const getDescriptionItem = (
|
|||
return [];
|
||||
}
|
||||
} else if (field === 'eqlOptions') {
|
||||
const eqlOptions: EqlOptionsSelected = get(field, data);
|
||||
const eqlOptions: EqlOptions = get(field, data);
|
||||
return buildEqlOptionsDescription(eqlOptions);
|
||||
} else if (field === 'threat') {
|
||||
const threats: Threats = get(field, data);
|
||||
|
|
|
@ -1,137 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import type { FieldHook, ValidationError, ValidationFunc } from '../../../../shared_imports';
|
||||
import { isEqlRule } from '../../../../../common/detection_engine/utils';
|
||||
import { KibanaServices } from '../../../../common/lib/kibana';
|
||||
import type { DefineStepRule } from '../../../../detections/pages/detection_engine/rules/types';
|
||||
import { DataSourceType } from '../../../../detections/pages/detection_engine/rules/types';
|
||||
import type { EqlResponseError } from '../../../../common/hooks/eql/api';
|
||||
import { validateEql, EQL_ERROR_CODES } from '../../../../common/hooks/eql/api';
|
||||
import type { FieldValueQueryBar } from '../query_bar';
|
||||
import * as i18n from './translations';
|
||||
|
||||
/**
|
||||
* Unlike lodash's debounce, which resolves intermediate calls with the most
|
||||
* recent value, this implementation waits to resolve intermediate calls until
|
||||
* the next invocation resolves.
|
||||
*
|
||||
* @param fn an async function
|
||||
*
|
||||
* @returns A debounced async function that resolves on the next invocation
|
||||
*/
|
||||
export const debounceAsync = <Args extends unknown[], Result extends Promise<unknown>>(
|
||||
fn: (...args: Args) => Result,
|
||||
interval: number
|
||||
): ((...args: Args) => Result) => {
|
||||
let handle: ReturnType<typeof setTimeout> | undefined;
|
||||
let resolves: Array<(value?: Result) => void> = [];
|
||||
|
||||
return (...args: Args): Result => {
|
||||
if (handle) {
|
||||
clearTimeout(handle);
|
||||
}
|
||||
|
||||
handle = setTimeout(() => {
|
||||
const result = fn(...args);
|
||||
resolves.forEach((resolve) => resolve(result));
|
||||
resolves = [];
|
||||
}, interval);
|
||||
|
||||
return new Promise((resolve) => resolves.push(resolve)) as Result;
|
||||
};
|
||||
};
|
||||
|
||||
export const transformEqlResponseErrorToValidationError = (
|
||||
responseError: EqlResponseError
|
||||
): ValidationError<EQL_ERROR_CODES> => {
|
||||
if (responseError.error) {
|
||||
return {
|
||||
code: EQL_ERROR_CODES.FAILED_REQUEST,
|
||||
message: i18n.EQL_VALIDATION_REQUEST_ERROR,
|
||||
error: responseError.error,
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: responseError.code,
|
||||
message: '',
|
||||
messages: responseError.messages,
|
||||
};
|
||||
};
|
||||
|
||||
export const eqlValidator = async (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): Promise<ValidationError<EQL_ERROR_CODES> | void | undefined> => {
|
||||
const [{ value, formData }] = args;
|
||||
const { query: queryValue } = value as FieldValueQueryBar;
|
||||
const query = queryValue.query as string;
|
||||
const { dataViewId, index, ruleType, eqlOptions } = formData as DefineStepRule;
|
||||
|
||||
const needsValidation =
|
||||
(ruleType === undefined && !isEmpty(query)) || (isEqlRule(ruleType) && !isEmpty(query));
|
||||
if (!needsValidation) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = KibanaServices.get();
|
||||
let dataViewTitle = index?.join();
|
||||
let runtimeMappings = {};
|
||||
if (
|
||||
dataViewId != null &&
|
||||
dataViewId !== '' &&
|
||||
formData.dataSourceType === DataSourceType.DataView
|
||||
) {
|
||||
const dataView = await data.dataViews.get(dataViewId);
|
||||
|
||||
dataViewTitle = dataView.title;
|
||||
runtimeMappings = dataView.getRuntimeMappings();
|
||||
}
|
||||
|
||||
const signal = new AbortController().signal;
|
||||
const response = await validateEql({
|
||||
data,
|
||||
query,
|
||||
signal,
|
||||
dataViewTitle,
|
||||
runtimeMappings,
|
||||
options: eqlOptions,
|
||||
});
|
||||
|
||||
if (response?.valid === false && response.error) {
|
||||
return transformEqlResponseErrorToValidationError(response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
code: EQL_ERROR_CODES.FAILED_REQUEST,
|
||||
message: i18n.EQL_VALIDATION_REQUEST_ERROR,
|
||||
error,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getValidationResults = <T = unknown>(
|
||||
field: FieldHook<T>
|
||||
): { isValid: boolean; message: string; messages?: string[]; error?: Error } => {
|
||||
const hasErrors = field.errors.length > 0;
|
||||
const isValid = !field.isChangingValue && !hasErrors;
|
||||
|
||||
if (hasErrors) {
|
||||
const [error] = field.errors;
|
||||
const message = error.message;
|
||||
|
||||
if (error.code === EQL_ERROR_CODES.FAILED_REQUEST) {
|
||||
return { isValid, message, error: error.error };
|
||||
} else {
|
||||
return { isValid, message, messages: error.messages };
|
||||
}
|
||||
} else {
|
||||
return { isValid, message: '' };
|
||||
}
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { screen, fireEvent, render, within, act, waitFor } from '@testing-library/react';
|
||||
import type { Type as RuleType } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import type { DataViewBase } from '@kbn/es-query';
|
||||
|
@ -638,7 +638,6 @@ function TestForm({
|
|||
onSubmit,
|
||||
formProps,
|
||||
}: TestFormProps): JSX.Element {
|
||||
const [selectedEqlOptions, setSelectedEqlOptions] = useState(stepDefineDefaultValue.eqlOptions);
|
||||
const { form } = useForm({
|
||||
options: { stripEmptyFields: false },
|
||||
schema: defineRuleSchema,
|
||||
|
@ -653,8 +652,6 @@ function TestForm({
|
|||
form={form}
|
||||
indicesConfig={[]}
|
||||
threatIndicesConfig={[]}
|
||||
optionsSelected={selectedEqlOptions}
|
||||
setOptionsSelected={setSelectedEqlOptions}
|
||||
indexPattern={indexPattern}
|
||||
isIndexPatternLoading={false}
|
||||
isQueryBarValid={true}
|
||||
|
|
|
@ -20,7 +20,7 @@ import React, { memo, useCallback, useState, useEffect, useMemo, useRef } from '
|
|||
|
||||
import styled from 'styled-components';
|
||||
import { i18n as i18nCore } from '@kbn/i18n';
|
||||
import { isEqual, isEmpty } from 'lodash';
|
||||
import { isEqual } from 'lodash';
|
||||
import type { FieldSpec } from '@kbn/data-plugin/common';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
|
@ -33,7 +33,6 @@ import { useSetFieldValueWithCallback } from '../../../../common/utils/use_set_f
|
|||
import type { SetRuleQuery } from '../../../../detections/containers/detection_engine/rules/use_rule_from_timeline';
|
||||
import { useRuleFromTimeline } from '../../../../detections/containers/detection_engine/rules/use_rule_from_timeline';
|
||||
import { isMlRule } from '../../../../../common/machine_learning/helpers';
|
||||
import type { EqlOptionsSelected, FieldsEqlOptions } from '../../../../../common/search_strategy';
|
||||
import { filterRuleFieldsForType, getStepDataDataSource } from '../../pages/rule_creation/helpers';
|
||||
import type {
|
||||
DefineStepRule,
|
||||
|
@ -74,7 +73,7 @@ import {
|
|||
isEqlSequenceQuery,
|
||||
isSuppressionRuleInGA,
|
||||
} from '../../../../../common/detection_engine/utils';
|
||||
import { EqlQueryBar } from '../eql_query_bar';
|
||||
import { EqlQueryEdit } from '../../../rule_creation/components/eql_query_edit';
|
||||
import { DataViewSelectorField } from '../data_view_selector_field';
|
||||
import { ThreatMatchInput } from '../threatmatch_input';
|
||||
import { useFetchIndex } from '../../../../common/containers/source';
|
||||
|
@ -91,7 +90,10 @@ import { useAlertSuppression } from '../../../rule_management/logic/use_alert_su
|
|||
import { AiAssistant } from '../ai_assistant';
|
||||
import { RelatedIntegrations } from '../../../rule_creation/components/related_integrations';
|
||||
import { useMLRuleConfig } from '../../../../common/components/ml/hooks/use_ml_rule_config';
|
||||
import { AlertSuppressionEdit } from '../../../rule_creation/components/alert_suppression_edit';
|
||||
import {
|
||||
ALERT_SUPPRESSION_FIELDS_FIELD_NAME,
|
||||
AlertSuppressionEdit,
|
||||
} from '../../../rule_creation/components/alert_suppression_edit';
|
||||
import { ThresholdAlertSuppressionEdit } from '../../../rule_creation/components/threshold_alert_suppression_edit';
|
||||
import { usePersistentAlertSuppressionState } from './use_persistent_alert_suppression_state';
|
||||
|
||||
|
@ -105,8 +107,6 @@ export interface StepDefineRuleProps extends RuleStepProps {
|
|||
threatIndicesConfig: string[];
|
||||
defaultSavedQuery?: SavedQuery;
|
||||
form: FormHook<DefineStepRule>;
|
||||
optionsSelected: EqlOptionsSelected;
|
||||
setOptionsSelected: React.Dispatch<React.SetStateAction<EqlOptionsSelected>>;
|
||||
indexPattern: DataViewBase;
|
||||
isIndexPatternLoading: boolean;
|
||||
isQueryBarValid: boolean;
|
||||
|
@ -163,13 +163,11 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
isLoading,
|
||||
isQueryBarValid,
|
||||
isUpdateView = false,
|
||||
optionsSelected,
|
||||
queryBarSavedId,
|
||||
queryBarTitle,
|
||||
ruleType,
|
||||
setIsQueryBarValid,
|
||||
setIsThreatQueryBarValid,
|
||||
setOptionsSelected,
|
||||
shouldLoadQueryDynamically,
|
||||
threatIndex,
|
||||
threatIndicesConfig,
|
||||
|
@ -220,15 +218,12 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
};
|
||||
if (timelineQueryBar.query.language === 'eql') {
|
||||
setRuleTypeCallback('eql', setQuery);
|
||||
setOptionsSelected((prevOptions) => ({
|
||||
...prevOptions,
|
||||
...(eqlOptions != null ? eqlOptions : {}),
|
||||
}));
|
||||
setFieldValue('eqlOptions', eqlOptions ?? {});
|
||||
} else {
|
||||
setQuery();
|
||||
}
|
||||
},
|
||||
[setFieldValue, setRuleTypeCallback, setOptionsSelected]
|
||||
[setFieldValue, setRuleTypeCallback]
|
||||
);
|
||||
|
||||
const { onOpenTimeline, loading: timelineQueryLoading } =
|
||||
|
@ -719,43 +714,6 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
]
|
||||
);
|
||||
|
||||
const onOptionsChange = useCallback(
|
||||
(field: FieldsEqlOptions, value: string | undefined) => {
|
||||
setOptionsSelected((prevOptions) => {
|
||||
const newOptions = {
|
||||
...prevOptions,
|
||||
[field]: value,
|
||||
};
|
||||
|
||||
setFieldValue('eqlOptions', newOptions);
|
||||
return newOptions;
|
||||
});
|
||||
},
|
||||
[setFieldValue, setOptionsSelected]
|
||||
);
|
||||
|
||||
const optionsData = useMemo(
|
||||
() =>
|
||||
isEmpty(indexPattern.fields)
|
||||
? {
|
||||
keywordFields: [],
|
||||
dateFields: [],
|
||||
nonDateFields: [],
|
||||
}
|
||||
: {
|
||||
keywordFields: (indexPattern.fields as FieldSpec[])
|
||||
.filter((f) => f.esTypes?.includes('keyword'))
|
||||
.map((f) => ({ label: f.name })),
|
||||
dateFields: indexPattern.fields
|
||||
.filter((f) => f.type === 'date')
|
||||
.map((f) => ({ label: f.name })),
|
||||
nonDateFields: indexPattern.fields
|
||||
.filter((f) => f.type !== 'date')
|
||||
.map((f) => ({ label: f.name })),
|
||||
},
|
||||
[indexPattern]
|
||||
);
|
||||
|
||||
const selectRuleTypeProps = useMemo(
|
||||
() => ({
|
||||
describedByIds: ['detectionEngineStepDefineRuleType'],
|
||||
|
@ -794,29 +752,18 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
<EuiSpacer size="s" />
|
||||
{isEqlRule(ruleType) ? (
|
||||
<>
|
||||
<UseField
|
||||
key="EqlQueryBar"
|
||||
<EqlQueryEdit
|
||||
key="eqlQueryBar"
|
||||
path="queryBar"
|
||||
component={EqlQueryBar}
|
||||
componentProps={{
|
||||
optionsData,
|
||||
optionsSelected,
|
||||
isSizeOptionDisabled: true,
|
||||
onOptionsChange,
|
||||
onValidityChange: setIsQueryBarValid,
|
||||
idAria: 'detectionEngineStepDefineRuleEqlQueryBar',
|
||||
isDisabled: isLoading,
|
||||
isLoading: isIndexPatternLoading,
|
||||
indexPattern,
|
||||
showFilterBar: true,
|
||||
dataTestSubj: 'detectionEngineStepDefineRuleEqlQueryBar',
|
||||
}}
|
||||
config={{
|
||||
...schema.queryBar,
|
||||
label: i18n.EQL_QUERY_BAR_LABEL,
|
||||
}}
|
||||
eqlOptionsPath="eqlOptions"
|
||||
fieldsToValidateOnChange={ALERT_SUPPRESSION_FIELDS_FIELD_NAME}
|
||||
required
|
||||
showFilterBar
|
||||
dataView={indexPattern}
|
||||
loading={isIndexPatternLoading}
|
||||
disabled={isLoading}
|
||||
onValidityChange={setIsQueryBarValid}
|
||||
/>
|
||||
<UseField path="eqlOptions" component={HiddenField} />
|
||||
</>
|
||||
) : isEsqlRule(ruleType) ? (
|
||||
EsqlQueryBarMemo
|
||||
|
|
|
@ -9,8 +9,7 @@ import { isEmpty } from 'lodash';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { fromKueryExpression } from '@kbn/es-query';
|
||||
import { debounceAsync } from '@kbn/securitysolution-utils';
|
||||
import {
|
||||
singleEntryThreat,
|
||||
containsInvalidItems,
|
||||
|
@ -27,32 +26,29 @@ import {
|
|||
} from '../../../../../common/detection_engine/utils';
|
||||
import { MAX_NUMBER_OF_NEW_TERMS_FIELDS } from '../../../../../common/constants';
|
||||
import { isMlRule } from '../../../../../common/machine_learning/helpers';
|
||||
import type { FieldValueQueryBar } from '../query_bar';
|
||||
import type { ERROR_CODE, FormSchema, ValidationFunc } from '../../../../shared_imports';
|
||||
import { FIELD_TYPES, fieldValidators } from '../../../../shared_imports';
|
||||
import type { DefineStepRule } from '../../../../detections/pages/detection_engine/rules/types';
|
||||
import { DataSourceType } from '../../../../detections/pages/detection_engine/rules/types';
|
||||
import { debounceAsync, eqlValidator } from '../eql_query_bar/validators';
|
||||
import { esqlValidator } from '../../../rule_creation/logic/esql_validator';
|
||||
import { dataViewIdValidatorFactory } from '../../validators/data_view_id_validator_factory';
|
||||
import { indexPatternValidatorFactory } from '../../validators/index_pattern_validator_factory';
|
||||
import { alertSuppressionFieldsValidatorFactory } from '../../validators/alert_suppression_fields_validator_factory';
|
||||
import {
|
||||
CUSTOM_QUERY_REQUIRED,
|
||||
INVALID_CUSTOM_QUERY,
|
||||
INDEX_HELPER_TEXT,
|
||||
THREAT_MATCH_INDEX_HELPER_TEXT,
|
||||
THREAT_MATCH_REQUIRED,
|
||||
THREAT_MATCH_EMPTIES,
|
||||
EQL_SEQUENCE_SUPPRESSION_GROUPBY_VALIDATION_TEXT,
|
||||
} from './translations';
|
||||
import { getQueryRequiredMessage } from './utils';
|
||||
import {
|
||||
ALERT_SUPPRESSION_DURATION_FIELD_NAME,
|
||||
ALERT_SUPPRESSION_FIELDS_FIELD_NAME,
|
||||
ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME,
|
||||
} from '../../../rule_creation/components/alert_suppression_edit';
|
||||
import * as alertSuppressionEditI81n from '../../../rule_creation/components/alert_suppression_edit/components/translations';
|
||||
import { queryRequiredValidatorFactory } from '../../validators/query_required_validator_factory';
|
||||
import { kueryValidatorFactory } from '../../validators/kuery_validator_factory';
|
||||
|
||||
export const schema: FormSchema<DefineStepRule> = {
|
||||
index: {
|
||||
|
@ -68,7 +64,7 @@ export const schema: FormSchema<DefineStepRule> = {
|
|||
helpText: <EuiText size="xs">{INDEX_HELPER_TEXT}</EuiText>,
|
||||
validations: [
|
||||
{
|
||||
validator: (...args: Parameters<ValidationFunc>) => {
|
||||
validator: (...args) => {
|
||||
const [{ formData }] = args;
|
||||
|
||||
if (
|
||||
|
@ -94,7 +90,7 @@ export const schema: FormSchema<DefineStepRule> = {
|
|||
fieldsToValidateOnChange: ['dataViewId'],
|
||||
validations: [
|
||||
{
|
||||
validator: (...args: Parameters<ValidationFunc>) => {
|
||||
validator: (...args) => {
|
||||
const [{ formData }] = args;
|
||||
|
||||
if (isMlRule(formData.ruleType) || formData.dataSourceType !== DataSourceType.DataView) {
|
||||
|
@ -122,55 +118,21 @@ export const schema: FormSchema<DefineStepRule> = {
|
|||
fieldsToValidateOnChange: ['queryBar', ALERT_SUPPRESSION_FIELDS_FIELD_NAME],
|
||||
validations: [
|
||||
{
|
||||
validator: (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => {
|
||||
const [{ value, path, formData }] = args;
|
||||
const { query, filters, saved_id: savedId } = value as FieldValueQueryBar;
|
||||
const needsValidation = !isMlRule(formData.ruleType);
|
||||
if (!needsValidation) {
|
||||
return undefined;
|
||||
}
|
||||
const isFieldEmpty = isEmpty(query.query as string) && isEmpty(filters);
|
||||
if (!isFieldEmpty) {
|
||||
return undefined;
|
||||
}
|
||||
if (savedId) {
|
||||
validator: (...args) => {
|
||||
const [{ value, formData }] = args;
|
||||
|
||||
if (isMlRule(formData.ruleType) || value.saved_id) {
|
||||
// Ignore field validation error in this case.
|
||||
// Instead, we show the error toast when saved query object does not exist.
|
||||
// https://github.com/elastic/kibana/issues/159060
|
||||
return undefined;
|
||||
}
|
||||
const message = getQueryRequiredMessage(formData.ruleType);
|
||||
return { code: 'ERR_FIELD_MISSING', path, message };
|
||||
},
|
||||
},
|
||||
{
|
||||
validator: (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => {
|
||||
const [{ value, path, formData }] = args;
|
||||
const { query } = value as FieldValueQueryBar;
|
||||
const needsValidation = !isMlRule(formData.ruleType);
|
||||
if (!needsValidation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEmpty(query.query as string) && query.language === 'kuery') {
|
||||
try {
|
||||
fromKueryExpression(query.query);
|
||||
} catch (err) {
|
||||
return {
|
||||
code: 'ERR_FIELD_FORMAT',
|
||||
path,
|
||||
message: INVALID_CUSTOM_QUERY,
|
||||
};
|
||||
}
|
||||
}
|
||||
return queryRequiredValidatorFactory(formData.ruleType)(...args);
|
||||
},
|
||||
},
|
||||
{
|
||||
validator: debounceAsync(eqlValidator, 300),
|
||||
validator: kueryValidatorFactory(),
|
||||
},
|
||||
{
|
||||
validator: debounceAsync(esqlValidator, 300),
|
||||
|
@ -509,49 +471,17 @@ export const schema: FormSchema<DefineStepRule> = {
|
|||
),
|
||||
validations: [
|
||||
{
|
||||
validator: (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => {
|
||||
const [{ value, path, formData }] = args;
|
||||
const needsValidation = isThreatMatchRule(formData.ruleType);
|
||||
if (!needsValidation) {
|
||||
validator: (...args) => {
|
||||
const [{ formData }] = args;
|
||||
if (!isThreatMatchRule(formData.ruleType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { query, filters } = value as FieldValueQueryBar;
|
||||
|
||||
return isEmpty(query.query as string) && isEmpty(filters)
|
||||
? {
|
||||
code: 'ERR_FIELD_MISSING',
|
||||
path,
|
||||
message: CUSTOM_QUERY_REQUIRED,
|
||||
}
|
||||
: undefined;
|
||||
return queryRequiredValidatorFactory(formData.ruleType)(...args);
|
||||
},
|
||||
},
|
||||
{
|
||||
validator: (
|
||||
...args: Parameters<ValidationFunc>
|
||||
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => {
|
||||
const [{ value, path, formData }] = args;
|
||||
const needsValidation = isThreatMatchRule(formData.ruleType);
|
||||
if (!needsValidation) {
|
||||
return;
|
||||
}
|
||||
const { query } = value as FieldValueQueryBar;
|
||||
|
||||
if (!isEmpty(query.query as string) && query.language === 'kuery') {
|
||||
try {
|
||||
fromKueryExpression(query.query);
|
||||
} catch (err) {
|
||||
return {
|
||||
code: 'ERR_FIELD_FORMAT',
|
||||
path,
|
||||
message: INVALID_CUSTOM_QUERY,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
validator: kueryValidatorFactory(),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -9,34 +9,6 @@ import React from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
export const CUSTOM_QUERY_REQUIRED = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError',
|
||||
{
|
||||
defaultMessage: 'A custom query is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const EQL_QUERY_REQUIRED = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlQueryFieldRequiredError',
|
||||
{
|
||||
defaultMessage: 'An EQL query is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ESQL_QUERY_REQUIRED = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlQueryFieldRequiredError',
|
||||
{
|
||||
defaultMessage: 'An ES|QL query is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const INVALID_CUSTOM_QUERY = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldInvalidError',
|
||||
{
|
||||
defaultMessage: 'The KQL is invalid',
|
||||
}
|
||||
);
|
||||
|
||||
export const INDEX_HELPER_TEXT = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesHelperDescription',
|
||||
{
|
||||
|
@ -66,13 +38,6 @@ export const QUERY_BAR_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const EQL_QUERY_BAR_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.EqlQueryBarLabel',
|
||||
{
|
||||
defaultMessage: 'EQL query',
|
||||
}
|
||||
);
|
||||
|
||||
export const SAVED_QUERY_FORM_ROW_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.SavedQueryFormRowLabel',
|
||||
{
|
||||
|
|
|
@ -5,13 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import type { FieldSpec } from '@kbn/data-plugin/common';
|
||||
|
||||
import { CUSTOM_QUERY_REQUIRED, EQL_QUERY_REQUIRED, ESQL_QUERY_REQUIRED } from './translations';
|
||||
|
||||
import { isEqlRule, isEsqlRule } from '../../../../../common/detection_engine/utils';
|
||||
|
||||
/**
|
||||
* Filters out fields, that are not supported in terms aggregation.
|
||||
* Terms aggregation supports limited number of types:
|
||||
|
@ -25,18 +20,3 @@ export const getTermsAggregationFields = (fields: FieldSpec[]): FieldSpec[] =>
|
|||
|
||||
// binary types is excluded, as binary field has property aggregatable === false
|
||||
const ALLOWED_AGGREGATABLE_FIELD_TYPES_SET = new Set(['string', 'number', 'ip', 'boolean']);
|
||||
|
||||
/**
|
||||
* return query is required message depends on a rule type
|
||||
*/
|
||||
export const getQueryRequiredMessage = (ruleType: Type) => {
|
||||
if (isEsqlRule(ruleType)) {
|
||||
return ESQL_QUERY_REQUIRED;
|
||||
}
|
||||
|
||||
if (isEqlRule(ruleType)) {
|
||||
return EQL_QUERY_REQUIRED;
|
||||
}
|
||||
|
||||
return CUSTOM_QUERY_REQUIRED;
|
||||
};
|
||||
|
|
|
@ -17,7 +17,6 @@ import type {
|
|||
} from '../../../detections/pages/detection_engine/rules/types';
|
||||
|
||||
import { useRuleFormsErrors } from './form';
|
||||
import { transformEqlResponseErrorToValidationError } from '../components/eql_query_bar/validators';
|
||||
import { ALERT_SUPPRESSION_FIELDS_FIELD_NAME } from '../../rule_creation/components/alert_suppression_edit';
|
||||
|
||||
const getFormWithErrorsMock = <T extends FormData = FormData>(fields: {
|
||||
|
@ -33,13 +32,15 @@ describe('useRuleFormsErrors', () => {
|
|||
it('should return blocking error in case of syntax validation error', async () => {
|
||||
const { result } = renderHook(() => useRuleFormsErrors());
|
||||
|
||||
const validationError = transformEqlResponseErrorToValidationError({
|
||||
code: EQL_ERROR_CODES.INVALID_SYNTAX,
|
||||
messages: ["line 1:5: missing 'where' at 'demo'"],
|
||||
});
|
||||
const defineStepForm = getFormWithErrorsMock<DefineStepRule>({
|
||||
queryBar: {
|
||||
errors: [validationError],
|
||||
errors: [
|
||||
{
|
||||
code: EQL_ERROR_CODES.INVALID_SYNTAX,
|
||||
message: '',
|
||||
messages: ["line 1:5: missing 'where' at 'demo'"],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -53,13 +54,17 @@ describe('useRuleFormsErrors', () => {
|
|||
it('should return non-blocking error in case of missing data source validation error', async () => {
|
||||
const { result } = renderHook(() => useRuleFormsErrors());
|
||||
|
||||
const validationError = transformEqlResponseErrorToValidationError({
|
||||
code: EQL_ERROR_CODES.MISSING_DATA_SOURCE,
|
||||
messages: ['index_not_found_exception Found 1 problem line -1:-1: Unknown index [*,-*]'],
|
||||
});
|
||||
const defineStepForm = getFormWithErrorsMock<DefineStepRule>({
|
||||
queryBar: {
|
||||
errors: [validationError],
|
||||
errors: [
|
||||
{
|
||||
code: EQL_ERROR_CODES.MISSING_DATA_SOURCE,
|
||||
message: '',
|
||||
messages: [
|
||||
'index_not_found_exception Found 1 problem line -1:-1: Unknown index [*,-*]',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -75,15 +80,17 @@ describe('useRuleFormsErrors', () => {
|
|||
it('should return non-blocking error in case of missing data field validation error', async () => {
|
||||
const { result } = renderHook(() => useRuleFormsErrors());
|
||||
|
||||
const validationError = transformEqlResponseErrorToValidationError({
|
||||
code: EQL_ERROR_CODES.INVALID_EQL,
|
||||
messages: [
|
||||
'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
|
||||
],
|
||||
});
|
||||
const defineStepForm = getFormWithErrorsMock<DefineStepRule>({
|
||||
queryBar: {
|
||||
errors: [validationError],
|
||||
errors: [
|
||||
{
|
||||
code: EQL_ERROR_CODES.INVALID_EQL,
|
||||
message: '',
|
||||
messages: [
|
||||
'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -99,13 +106,15 @@ describe('useRuleFormsErrors', () => {
|
|||
it('should return non-blocking error in case of failed request error', async () => {
|
||||
const { result } = renderHook(() => useRuleFormsErrors());
|
||||
|
||||
const validationError = transformEqlResponseErrorToValidationError({
|
||||
code: EQL_ERROR_CODES.FAILED_REQUEST,
|
||||
error: new Error('Some internal error'),
|
||||
});
|
||||
const defineStepForm = getFormWithErrorsMock<DefineStepRule>({
|
||||
queryBar: {
|
||||
errors: [validationError],
|
||||
errors: [
|
||||
{
|
||||
code: EQL_ERROR_CODES.FAILED_REQUEST,
|
||||
message: 'An error occurred while validating your EQL query',
|
||||
error: new Error('Some internal error'),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -121,13 +130,15 @@ describe('useRuleFormsErrors', () => {
|
|||
it('should return blocking and non-blocking errors', async () => {
|
||||
const { result } = renderHook(() => useRuleFormsErrors());
|
||||
|
||||
const validationError = transformEqlResponseErrorToValidationError({
|
||||
code: EQL_ERROR_CODES.MISSING_DATA_SOURCE,
|
||||
messages: ['Missing data source'],
|
||||
});
|
||||
const defineStepForm = getFormWithErrorsMock<DefineStepRule>({
|
||||
queryBar: {
|
||||
errors: [validationError],
|
||||
errors: [
|
||||
{
|
||||
code: EQL_ERROR_CODES.MISSING_DATA_SOURCE,
|
||||
message: '',
|
||||
messages: ['Missing data source'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const aboutStepForm = getFormWithErrorsMock<AboutStepRule>({
|
||||
|
|
|
@ -19,7 +19,6 @@ import { useKibana } from '../../../common/lib/kibana';
|
|||
import type { FormHook, ValidationError } from '../../../shared_imports';
|
||||
import { useForm, useFormData } from '../../../shared_imports';
|
||||
import { schema as defineRuleSchema } from '../components/step_define_rule/schema';
|
||||
import type { EqlOptionsSelected } from '../../../../common/search_strategy';
|
||||
import {
|
||||
schema as aboutRuleSchema,
|
||||
threatMatchAboutSchema,
|
||||
|
@ -53,20 +52,14 @@ export const useRuleForms = ({
|
|||
options: { stripEmptyFields: false },
|
||||
schema: defineRuleSchema,
|
||||
});
|
||||
const [eqlOptionsSelected, setEqlOptionsSelected] = useState<EqlOptionsSelected>(
|
||||
defineStepDefault.eqlOptions
|
||||
);
|
||||
const [defineStepFormData] = useFormData<DefineStepRule | {}>({
|
||||
form: defineStepForm,
|
||||
});
|
||||
// FormData doesn't populate on the first render, so we use the defaultValue if the formData
|
||||
// doesn't have what we wanted
|
||||
const defineStepData = useMemo(
|
||||
() =>
|
||||
'index' in defineStepFormData
|
||||
? { ...defineStepFormData, eqlOptions: eqlOptionsSelected }
|
||||
: defineStepDefault,
|
||||
[defineStepDefault, defineStepFormData, eqlOptionsSelected]
|
||||
() => ('index' in defineStepFormData ? defineStepFormData : defineStepDefault),
|
||||
[defineStepDefault, defineStepFormData]
|
||||
);
|
||||
|
||||
// ABOUT STEP FORM
|
||||
|
@ -118,8 +111,6 @@ export const useRuleForms = ({
|
|||
scheduleStepData,
|
||||
actionsStepForm,
|
||||
actionsStepData,
|
||||
eqlOptionsSelected,
|
||||
setEqlOptionsSelected,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -171,8 +171,6 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
scheduleStepData,
|
||||
actionsStepForm,
|
||||
actionsStepData,
|
||||
eqlOptionsSelected,
|
||||
setEqlOptionsSelected,
|
||||
} = useRuleForms({
|
||||
defineStepDefault,
|
||||
aboutStepDefault: stepAboutDefaultValue,
|
||||
|
@ -392,10 +390,9 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
|
||||
const createRuleFromFormData = useCallback(
|
||||
async (enabled: boolean) => {
|
||||
const localDefineStepData: DefineStepRule = defineFieldsTransform({
|
||||
...defineStepForm.getFormData(),
|
||||
eqlOptions: eqlOptionsSelected,
|
||||
});
|
||||
const localDefineStepData: DefineStepRule = defineFieldsTransform(
|
||||
defineStepForm.getFormData()
|
||||
);
|
||||
const localAboutStepData = aboutStepForm.getFormData();
|
||||
const localScheduleStepData = scheduleStepForm.getFormData();
|
||||
const localActionsStepData = actionsStepForm.getFormData();
|
||||
|
@ -435,7 +432,6 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
createRule,
|
||||
defineFieldsTransform,
|
||||
defineStepForm,
|
||||
eqlOptionsSelected,
|
||||
navigateToApp,
|
||||
ruleType,
|
||||
scheduleStepForm,
|
||||
|
@ -556,8 +552,6 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
indicesConfig={indicesConfig}
|
||||
threatIndicesConfig={threatIndicesConfig}
|
||||
form={defineStepForm}
|
||||
optionsSelected={eqlOptionsSelected}
|
||||
setOptionsSelected={setEqlOptionsSelected}
|
||||
indexPattern={indexPattern}
|
||||
isIndexPatternLoading={isIndexPatternLoading}
|
||||
isQueryBarValid={isQueryBarValid}
|
||||
|
@ -588,7 +582,6 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
defineStepData,
|
||||
memoizedIndex,
|
||||
defineStepForm,
|
||||
eqlOptionsSelected,
|
||||
indexPattern,
|
||||
indicesConfig,
|
||||
isCreateRuleLoading,
|
||||
|
@ -596,7 +589,6 @@ const CreateRulePageComponent: React.FC = () => {
|
|||
isQueryBarValid,
|
||||
loading,
|
||||
memoDefineStepReadOnly,
|
||||
setEqlOptionsSelected,
|
||||
threatIndicesConfig,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -131,8 +131,6 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
|
|||
scheduleStepData,
|
||||
actionsStepForm,
|
||||
actionsStepData,
|
||||
eqlOptionsSelected,
|
||||
setEqlOptionsSelected,
|
||||
} = useRuleForms({
|
||||
defineStepDefault: defineRuleData,
|
||||
aboutStepDefault: aboutRuleData,
|
||||
|
@ -232,8 +230,6 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
|
|||
threatIndicesConfig={threatIndicesConfig}
|
||||
defaultSavedQuery={savedQuery}
|
||||
form={defineStepForm}
|
||||
optionsSelected={eqlOptionsSelected}
|
||||
setOptionsSelected={setEqlOptionsSelected}
|
||||
key="defineStep"
|
||||
indexPattern={indexPattern}
|
||||
isIndexPatternLoading={isIndexPatternLoading}
|
||||
|
@ -355,8 +351,6 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => {
|
|||
threatIndicesConfig,
|
||||
savedQuery,
|
||||
defineStepForm,
|
||||
eqlOptionsSelected,
|
||||
setEqlOptionsSelected,
|
||||
indexPattern,
|
||||
isIndexPatternLoading,
|
||||
isQueryBarValid,
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { fromKueryExpression } from '@kbn/es-query';
|
||||
import type { FormData, ValidationFunc } from '../../../shared_imports';
|
||||
import type { FieldValueQueryBar } from '../components/query_bar';
|
||||
|
||||
export function kueryValidatorFactory(): ValidationFunc<FormData, string, FieldValueQueryBar> {
|
||||
return (...args) => {
|
||||
const [{ path, value }] = args;
|
||||
|
||||
if (isEmpty(value.query.query) || value.query.language !== 'kuery') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
fromKueryExpression(value.query.query);
|
||||
} catch (err) {
|
||||
return {
|
||||
code: 'ERR_FIELD_FORMAT',
|
||||
path,
|
||||
message: i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldInvalidError',
|
||||
{
|
||||
defaultMessage: 'The KQL is invalid',
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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';
|
||||
import { isEmpty } from 'lodash';
|
||||
import type { RuleType } from '@kbn/securitysolution-rules';
|
||||
import type { FormData, ValidationFunc } from '../../../shared_imports';
|
||||
import { isEqlRule, isEsqlRule } from '../../../../common/detection_engine/utils';
|
||||
import type { FieldValueQueryBar } from '../components/query_bar';
|
||||
|
||||
export function queryRequiredValidatorFactory(
|
||||
ruleType: RuleType
|
||||
): ValidationFunc<FormData, string, FieldValueQueryBar> {
|
||||
return (...args) => {
|
||||
const [{ path, value }] = args;
|
||||
|
||||
if (isEmpty(value.query.query as string) && isEmpty(value.filters)) {
|
||||
return {
|
||||
code: 'ERR_FIELD_MISSING',
|
||||
path,
|
||||
message: getErrorMessage(ruleType),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getErrorMessage(ruleType: RuleType): string {
|
||||
if (isEsqlRule(ruleType)) {
|
||||
return i18n.translate(
|
||||
'xpack.securitySolution.ruleManagement.ruleCreation.validation.query.esqlQueryFieldRequiredError',
|
||||
{
|
||||
defaultMessage: 'An ES|QL query is required.',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (isEqlRule(ruleType)) {
|
||||
return i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlQueryFieldRequiredError',
|
||||
{
|
||||
defaultMessage: 'An EQL query is required.',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.customQueryFieldRequiredError',
|
||||
{
|
||||
defaultMessage: 'A custom query is required.',
|
||||
}
|
||||
);
|
||||
}
|
|
@ -34,9 +34,6 @@ export const DEFINITION_UPGRADE_FIELD_ORDER: Array<keyof DiffableAllFields> = [
|
|||
'type',
|
||||
'kql_query',
|
||||
'eql_query',
|
||||
'event_category_override',
|
||||
'timestamp_field',
|
||||
'tiebreaker_field',
|
||||
'esql_query',
|
||||
'anomaly_threshold',
|
||||
'machine_learning_job_id',
|
||||
|
|
|
@ -92,16 +92,16 @@ describe('PerFieldRuleDiffTab', () => {
|
|||
});
|
||||
|
||||
describe('Undefined values are displayed with empty diffs', () => {
|
||||
test('Displays only an updated field value when changed from undefined', () => {
|
||||
test('Displays only an updated field value when changed from an empty value', () => {
|
||||
const mockData: PartialRuleDiff = {
|
||||
...ruleFieldsDiffMock,
|
||||
fields: {
|
||||
timestamp_field: {
|
||||
name: {
|
||||
...ruleFieldsDiffBaseFieldsMock,
|
||||
base_version: undefined,
|
||||
current_version: undefined,
|
||||
merged_version: 'new timestamp field',
|
||||
target_version: 'new timestamp field',
|
||||
current_version: '',
|
||||
merged_version: 'new name',
|
||||
target_version: 'new name',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -109,19 +109,19 @@ describe('PerFieldRuleDiffTab', () => {
|
|||
const diffContent = wrapper.getByTestId('ruleUpgradePerFieldDiffContent').textContent;
|
||||
|
||||
// Only the new timestamp field should be displayed
|
||||
expect(diffContent).toEqual('+new timestamp field');
|
||||
expect(diffContent).toEqual('+new name');
|
||||
});
|
||||
|
||||
test('Displays only an outdated field value when incoming update is undefined', () => {
|
||||
test('Displays only an outdated field value when incoming update is an empty value', () => {
|
||||
const mockData: PartialRuleDiff = {
|
||||
...ruleFieldsDiffMock,
|
||||
fields: {
|
||||
timestamp_field: {
|
||||
name: {
|
||||
...ruleFieldsDiffBaseFieldsMock,
|
||||
base_version: 'old timestamp field',
|
||||
current_version: 'old timestamp field',
|
||||
merged_version: undefined,
|
||||
target_version: undefined,
|
||||
base_version: 'old name',
|
||||
current_version: 'old name',
|
||||
merged_version: '',
|
||||
target_version: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -129,7 +129,7 @@ describe('PerFieldRuleDiffTab', () => {
|
|||
const diffContent = wrapper.getByTestId('ruleUpgradePerFieldDiffContent').textContent;
|
||||
|
||||
// Only the old timestamp_field should be displayed
|
||||
expect(diffContent).toEqual('-old timestamp field');
|
||||
expect(diffContent).toEqual('-old name');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -144,13 +144,6 @@ describe('PerFieldRuleDiffTab', () => {
|
|||
merged_version: 'new setup',
|
||||
target_version: 'new setup',
|
||||
},
|
||||
timestamp_field: {
|
||||
...ruleFieldsDiffBaseFieldsMock,
|
||||
base_version: undefined,
|
||||
current_version: undefined,
|
||||
merged_version: 'new timestamp',
|
||||
target_version: 'new timestamp',
|
||||
},
|
||||
name: {
|
||||
...ruleFieldsDiffBaseFieldsMock,
|
||||
base_version: 'old name',
|
||||
|
@ -166,11 +159,11 @@ describe('PerFieldRuleDiffTab', () => {
|
|||
const sectionLabels = matchedSectionElements.map((element) => element.textContent);
|
||||
|
||||
// Schedule doesn't have any fields in the diff and shouldn't be displayed
|
||||
expect(sectionLabels).toEqual(['About', 'Definition', 'Setup guide']);
|
||||
expect(sectionLabels).toEqual(['About', 'Setup guide']);
|
||||
|
||||
const matchedFieldElements = wrapper.queryAllByTestId('ruleUpgradePerFieldDiffLabel');
|
||||
const fieldLabels = matchedFieldElements.map((element) => element.textContent);
|
||||
|
||||
expect(fieldLabels).toEqual(['Name', 'Timestamp Field', 'Setup']);
|
||||
expect(fieldLabels).toEqual(['Name', 'Setup']);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ import type { DataView } from '@kbn/data-views-plugin/public';
|
|||
import { FilterItems } from '@kbn/unified-search-plugin/public';
|
||||
import type {
|
||||
AlertSuppressionMissingFieldsStrategy,
|
||||
EqlOptionalFields,
|
||||
RequiredFieldArray,
|
||||
RuleResponse,
|
||||
Threshold as ThresholdType,
|
||||
|
@ -59,6 +60,11 @@ import {
|
|||
} from './rule_definition_section.styles';
|
||||
import { getQueryLanguageLabel } from './helpers';
|
||||
import { useDefaultIndexPattern } from '../../hooks/use_default_index_pattern';
|
||||
import {
|
||||
EQL_OPTIONS_EVENT_CATEGORY_FIELD_LABEL,
|
||||
EQL_OPTIONS_EVENT_TIEBREAKER_FIELD_LABEL,
|
||||
EQL_OPTIONS_EVENT_TIMESTAMP_FIELD_LABEL,
|
||||
} from '../../../rule_creation/components/eql_query_edit/translations';
|
||||
|
||||
interface SavedQueryNameProps {
|
||||
savedQueryName: string;
|
||||
|
@ -562,6 +568,51 @@ const prepareDefinitionSectionListItems = (
|
|||
}
|
||||
}
|
||||
|
||||
if ((rule as EqlOptionalFields).event_category_override) {
|
||||
definitionSectionListItems.push({
|
||||
title: (
|
||||
<span data-test-subj="eqlOptionsEventCategoryOverrideTitle">
|
||||
{EQL_OPTIONS_EVENT_CATEGORY_FIELD_LABEL}
|
||||
</span>
|
||||
),
|
||||
description: (
|
||||
<span data-test-subj="eqlOptionsEventCategoryOverrideValue">
|
||||
{(rule as EqlOptionalFields).event_category_override}
|
||||
</span>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if ((rule as EqlOptionalFields).tiebreaker_field) {
|
||||
definitionSectionListItems.push({
|
||||
title: (
|
||||
<span data-test-subj="eqlOptionsTiebreakerFieldTitle">
|
||||
{EQL_OPTIONS_EVENT_TIEBREAKER_FIELD_LABEL}
|
||||
</span>
|
||||
),
|
||||
description: (
|
||||
<span data-test-subj="eqlOptionsEventTiebreakerFieldValue">
|
||||
{(rule as EqlOptionalFields).tiebreaker_field}
|
||||
</span>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if ((rule as EqlOptionalFields).timestamp_field) {
|
||||
definitionSectionListItems.push({
|
||||
title: (
|
||||
<span data-test-subj="eqlOptionsTimestampFieldTitle">
|
||||
{EQL_OPTIONS_EVENT_TIMESTAMP_FIELD_LABEL}
|
||||
</span>
|
||||
),
|
||||
description: (
|
||||
<span data-test-subj="eqlOptionsTimestampFieldValue">
|
||||
{(rule as EqlOptionalFields).timestamp_field}
|
||||
</span>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (rule.type) {
|
||||
definitionSectionListItems.push({
|
||||
title: i18n.RULE_TYPE_FIELD_LABEL,
|
||||
|
|
|
@ -15,26 +15,25 @@ export const getSubfieldChangesForEqlQuery = (
|
|||
): SubfieldChange[] => {
|
||||
const changes: SubfieldChange[] = [];
|
||||
|
||||
const oldQuery = stringifyToSortedJson(oldFieldValue?.query);
|
||||
const newQuery = stringifyToSortedJson(newFieldValue?.query);
|
||||
const subFieldNames: Array<keyof DiffableAllFields['eql_query']> = [
|
||||
'query',
|
||||
'filters',
|
||||
'event_category_override',
|
||||
'tiebreaker_field',
|
||||
'timestamp_field',
|
||||
];
|
||||
|
||||
if (oldQuery !== newQuery) {
|
||||
changes.push({
|
||||
subfieldName: 'query',
|
||||
oldSubfieldValue: oldQuery,
|
||||
newSubfieldValue: newQuery,
|
||||
});
|
||||
}
|
||||
for (const subFieldName of subFieldNames) {
|
||||
const oldValue = stringifyToSortedJson(oldFieldValue?.[subFieldName]);
|
||||
const newValue = stringifyToSortedJson(newFieldValue?.[subFieldName]);
|
||||
|
||||
const oldFilters = stringifyToSortedJson(oldFieldValue?.filters);
|
||||
const newFilters = stringifyToSortedJson(newFieldValue?.filters);
|
||||
|
||||
if (oldFilters !== newFilters) {
|
||||
changes.push({
|
||||
subfieldName: 'filters',
|
||||
oldSubfieldValue: oldFilters,
|
||||
newSubfieldValue: newFilters,
|
||||
});
|
||||
if (newValue !== oldValue) {
|
||||
changes.push({
|
||||
subfieldName: subFieldName,
|
||||
oldSubfieldValue: oldValue,
|
||||
newSubfieldValue: newValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { assertUnreachable } from '../../../../../../../common/utility_types';
|
||||
import type { UpgradeableCustomQueryFields } from '../../../../model/prebuilt_rule_upgrade/fields';
|
||||
import { KqlQueryEditForm } from './fields/kql_query';
|
||||
import { DataSourceEditForm } from './fields/data_source';
|
||||
|
@ -24,6 +25,6 @@ export function CustomQueryRuleFieldEdit({ fieldName }: CustomQueryRuleFieldEdit
|
|||
case 'alert_suppression':
|
||||
return <AlertSuppressionEditForm />;
|
||||
default:
|
||||
return null; // Will be replaced with `assertUnreachable(fieldName)` once all fields are implemented
|
||||
return assertUnreachable(fieldName);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,11 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { assertUnreachable } from '../../../../../../../common/utility_types';
|
||||
import type { UpgradeableEqlFields } from '../../../../model/prebuilt_rule_upgrade/fields';
|
||||
import { DataSourceEditForm } from './fields/data_source';
|
||||
import { AlertSuppressionEditForm } from './fields/alert_suppression';
|
||||
import { EqlQueryEditForm } from './fields/eql_query';
|
||||
|
||||
interface EqlRuleFieldEditProps {
|
||||
fieldName: UpgradeableEqlFields;
|
||||
|
@ -16,11 +18,13 @@ interface EqlRuleFieldEditProps {
|
|||
|
||||
export function EqlRuleFieldEdit({ fieldName }: EqlRuleFieldEditProps) {
|
||||
switch (fieldName) {
|
||||
case 'eql_query':
|
||||
return <EqlQueryEditForm />;
|
||||
case 'data_source':
|
||||
return <DataSourceEditForm />;
|
||||
case 'alert_suppression':
|
||||
return <AlertSuppressionEditForm />;
|
||||
default:
|
||||
return null; // Will be replaced with `assertUnreachable(fieldName)` once all fields are implemented
|
||||
return assertUnreachable(fieldName);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 type { DataViewBase } from '@kbn/es-query';
|
||||
import { EqlQueryEdit } from '../../../../../../../rule_creation/components/eql_query_edit';
|
||||
import type { RuleFieldEditComponentProps } from '../rule_field_edit_component_props';
|
||||
import { useDiffableRuleDataView } from '../hooks/use_diffable_rule_data_view';
|
||||
|
||||
export function EqlQueryEditAdapter({
|
||||
finalDiffableRule,
|
||||
}: RuleFieldEditComponentProps): JSX.Element | null {
|
||||
const { dataView, isLoading } = useDiffableRuleDataView(finalDiffableRule);
|
||||
|
||||
// Wait for dataView to be defined to trigger validation with the correct index patterns
|
||||
if (!dataView) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EqlQueryEdit
|
||||
path="eqlQuery"
|
||||
eqlOptionsPath="eqlOptions"
|
||||
required
|
||||
dataView={dataView ?? DEFAULT_DATA_VIEW_BASE}
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
skipEqlValidation
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const DEFAULT_DATA_VIEW_BASE: DataViewBase = {
|
||||
title: '',
|
||||
fields: [],
|
||||
};
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 type { Filter } from '@kbn/es-query';
|
||||
import type { EqlOptions } from '@kbn/timelines-plugin/common';
|
||||
import type { FormData, FormSchema } from '../../../../../../../../shared_imports';
|
||||
import { RuleFieldEditFormWrapper } from '../rule_field_edit_form_wrapper';
|
||||
import type { FieldValueQueryBar } from '../../../../../../../rule_creation_ui/components/query_bar';
|
||||
import {
|
||||
type DiffableRule,
|
||||
RuleEqlQuery,
|
||||
QueryLanguageEnum,
|
||||
} from '../../../../../../../../../common/api/detection_engine';
|
||||
import { EqlQueryEditAdapter } from './eql_query_edit_adapter';
|
||||
|
||||
export function EqlQueryEditForm(): JSX.Element {
|
||||
return (
|
||||
<RuleFieldEditFormWrapper
|
||||
component={EqlQueryEditAdapter}
|
||||
ruleFieldFormSchema={kqlQuerySchema}
|
||||
deserializer={deserializer}
|
||||
serializer={serializer}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const kqlQuerySchema = {} as FormSchema<{
|
||||
eqlQuery: RuleEqlQuery;
|
||||
}>;
|
||||
|
||||
function deserializer(
|
||||
fieldValue: FormData,
|
||||
finalDiffableRule: DiffableRule
|
||||
): {
|
||||
eqlQuery: FieldValueQueryBar;
|
||||
eqlOptions: EqlOptions;
|
||||
} {
|
||||
const parsedEqlQuery =
|
||||
'eql_query' in finalDiffableRule
|
||||
? RuleEqlQuery.parse(fieldValue.eql_query)
|
||||
: {
|
||||
query: '',
|
||||
language: QueryLanguageEnum.eql,
|
||||
filters: [],
|
||||
};
|
||||
|
||||
return {
|
||||
eqlQuery: {
|
||||
query: {
|
||||
query: parsedEqlQuery.query,
|
||||
language: parsedEqlQuery.language,
|
||||
},
|
||||
// cast to Filter since RuleEqlQuery checks it's an array
|
||||
// potentially it might be incompatible type
|
||||
filters: parsedEqlQuery.filters as Filter[],
|
||||
saved_id: null,
|
||||
},
|
||||
eqlOptions: {
|
||||
eventCategoryField: parsedEqlQuery.event_category_override,
|
||||
timestampField: parsedEqlQuery.timestamp_field,
|
||||
tiebreakerField: parsedEqlQuery.tiebreaker_field,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function serializer(formData: FormData): {
|
||||
eql_query: RuleEqlQuery;
|
||||
} {
|
||||
const formValue = formData as { eqlQuery: FieldValueQueryBar; eqlOptions: EqlOptions };
|
||||
|
||||
return {
|
||||
eql_query: {
|
||||
query: formValue.eqlQuery.query.query as string,
|
||||
language: QueryLanguageEnum.eql,
|
||||
filters: formValue.eqlQuery.filters,
|
||||
event_category_override: formValue.eqlOptions.eventCategoryField,
|
||||
timestamp_field: formValue.eqlOptions.timestampField,
|
||||
tiebreaker_field: formValue.eqlOptions.tiebreakerField,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 './eql_query_edit_form';
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { assertUnreachable } from '../../../../../../../common/utility_types';
|
||||
import type { UpgradeableSavedQueryFields } from '../../../../model/prebuilt_rule_upgrade/fields';
|
||||
import { KqlQueryEditForm } from './fields/kql_query';
|
||||
import { DataSourceEditForm } from './fields/data_source';
|
||||
|
@ -24,6 +25,6 @@ export function SavedQueryRuleFieldEdit({ fieldName }: SavedQueryRuleFieldEditPr
|
|||
case 'alert_suppression':
|
||||
return <AlertSuppressionEditForm />;
|
||||
default:
|
||||
return null; // Will be replaced with `assertUnreachable(fieldName)` once all fields are implemented
|
||||
return assertUnreachable(fieldName);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,6 @@ import { EqlQueryReadOnly } from './fields/eql_query/eql_query';
|
|||
import { TypeReadOnly } from './fields/type/type';
|
||||
import { AlertSuppressionReadOnly } from './fields/alert_suppression/alert_suppression';
|
||||
import { assertUnreachable } from '../../../../../../../common/utility_types';
|
||||
import { EventCategoryOverrideReadOnly } from './fields/event_category_override/event_category_override';
|
||||
import { TimestampFieldReadOnly } from './fields/timestamp_field/timestamp_field';
|
||||
import { TiebreakerFieldReadOnly } from './fields/tiebreaker_field/tiebreaker_field';
|
||||
|
||||
interface EqlRuleFieldReadOnlyProps {
|
||||
fieldName: keyof DiffableEqlFields;
|
||||
|
@ -39,16 +36,6 @@ export function EqlRuleFieldReadOnly({ fieldName, finalDiffableRule }: EqlRuleFi
|
|||
dataSource={finalDiffableRule.data_source}
|
||||
/>
|
||||
);
|
||||
case 'event_category_override':
|
||||
return (
|
||||
<EventCategoryOverrideReadOnly
|
||||
eventCategoryOverride={finalDiffableRule.event_category_override}
|
||||
/>
|
||||
);
|
||||
case 'tiebreaker_field':
|
||||
return <TiebreakerFieldReadOnly tiebreakerField={finalDiffableRule.tiebreaker_field} />;
|
||||
case 'timestamp_field':
|
||||
return <TimestampFieldReadOnly timestampField={finalDiffableRule.timestamp_field} />;
|
||||
case 'type':
|
||||
return <TypeReadOnly type={finalDiffableRule.type} />;
|
||||
default:
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiDescriptionList } from '@elastic/eui';
|
||||
import { EuiDescriptionList, EuiText } from '@elastic/eui';
|
||||
import type { EuiDescriptionListProps } from '@elastic/eui';
|
||||
import type {
|
||||
RuleDataSource,
|
||||
RuleEqlQuery,
|
||||
import {
|
||||
type RuleDataSource,
|
||||
type RuleEqlQuery,
|
||||
} from '../../../../../../../../../common/api/detection_engine';
|
||||
import * as descriptionStepI18n from '../../../../../../../rule_creation_ui/components/description_step/translations';
|
||||
import { Query, Filters } from '../../../../rule_definition_section';
|
||||
|
@ -38,5 +38,26 @@ export function EqlQueryReadOnly({ eqlQuery, dataSource }: EqlQueryReadOnlyProps
|
|||
});
|
||||
}
|
||||
|
||||
if (eqlQuery.event_category_override) {
|
||||
listItems.push({
|
||||
title: descriptionStepI18n.EQL_EVENT_CATEGORY_FIELD_LABEL,
|
||||
description: <EuiText size="s">{eqlQuery.event_category_override}</EuiText>,
|
||||
});
|
||||
}
|
||||
|
||||
if (eqlQuery.tiebreaker_field) {
|
||||
listItems.push({
|
||||
title: descriptionStepI18n.EQL_TIEBREAKER_FIELD_LABEL,
|
||||
description: <EuiText size="s">{eqlQuery.tiebreaker_field}</EuiText>,
|
||||
});
|
||||
}
|
||||
|
||||
if (eqlQuery.timestamp_field) {
|
||||
listItems.push({
|
||||
title: descriptionStepI18n.EQL_TIMESTAMP_FIELD_LABEL,
|
||||
description: <EuiText size="s">{eqlQuery.timestamp_field}</EuiText>,
|
||||
});
|
||||
}
|
||||
|
||||
return <EuiDescriptionList listItems={listItems} />;
|
||||
}
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { Story } from '@storybook/react';
|
||||
import { EventCategoryOverrideReadOnly } from './event_category_override';
|
||||
import { FieldReadOnly } from '../../field_readonly';
|
||||
import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine';
|
||||
import { mockEqlRule } from '../../storybook/mocks';
|
||||
import { ThreeWayDiffStorybookProviders } from '../../storybook/three_way_diff_storybook_providers';
|
||||
|
||||
export default {
|
||||
component: EventCategoryOverrideReadOnly,
|
||||
title:
|
||||
'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/event_category_override',
|
||||
};
|
||||
|
||||
interface TemplateProps {
|
||||
finalDiffableRule: DiffableRule;
|
||||
}
|
||||
|
||||
const Template: Story<TemplateProps> = (args) => {
|
||||
return (
|
||||
<ThreeWayDiffStorybookProviders finalDiffableRule={args.finalDiffableRule}>
|
||||
<FieldReadOnly fieldName="event_category_override" />
|
||||
</ThreeWayDiffStorybookProviders>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
Default.args = {
|
||||
finalDiffableRule: mockEqlRule({
|
||||
event_category_override: 'event.action',
|
||||
}),
|
||||
};
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiDescriptionList, EuiText } from '@elastic/eui';
|
||||
import * as descriptionStepI18n from '../../../../../../../rule_creation_ui/components/description_step/translations';
|
||||
import type { EventCategoryOverride as EventCategoryOverrideType } from '../../../../../../../../../common/api/detection_engine';
|
||||
|
||||
interface EventCategoryOverrideReadOnlyProps {
|
||||
eventCategoryOverride?: EventCategoryOverrideType;
|
||||
}
|
||||
|
||||
export function EventCategoryOverrideReadOnly({
|
||||
eventCategoryOverride,
|
||||
}: EventCategoryOverrideReadOnlyProps) {
|
||||
if (!eventCategoryOverride) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiDescriptionList
|
||||
listItems={[
|
||||
{
|
||||
title: descriptionStepI18n.EQL_EVENT_CATEGORY_FIELD_LABEL,
|
||||
description: <EventCategoryOverride eventCategoryOverride={eventCategoryOverride} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface EventCategoryOverrideProps {
|
||||
eventCategoryOverride: EventCategoryOverrideType;
|
||||
}
|
||||
|
||||
function EventCategoryOverride({ eventCategoryOverride }: EventCategoryOverrideProps) {
|
||||
return <EuiText size="s">{eventCategoryOverride}</EuiText>;
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { Story } from '@storybook/react';
|
||||
import { TiebreakerFieldReadOnly } from './tiebreaker_field';
|
||||
import { FieldReadOnly } from '../../field_readonly';
|
||||
import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine';
|
||||
import { mockEqlRule } from '../../storybook/mocks';
|
||||
import { ThreeWayDiffStorybookProviders } from '../../storybook/three_way_diff_storybook_providers';
|
||||
|
||||
export default {
|
||||
component: TiebreakerFieldReadOnly,
|
||||
title:
|
||||
'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/tiebreaker_field',
|
||||
};
|
||||
|
||||
interface TemplateProps {
|
||||
finalDiffableRule: DiffableRule;
|
||||
}
|
||||
|
||||
const Template: Story<TemplateProps> = (args) => {
|
||||
return (
|
||||
<ThreeWayDiffStorybookProviders finalDiffableRule={args.finalDiffableRule}>
|
||||
<FieldReadOnly fieldName="tiebreaker_field" />
|
||||
</ThreeWayDiffStorybookProviders>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
Default.args = {
|
||||
finalDiffableRule: mockEqlRule({
|
||||
tiebreaker_field: 'process.name',
|
||||
}),
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiDescriptionList, EuiText } from '@elastic/eui';
|
||||
import * as descriptionStepI18n from '../../../../../../../rule_creation_ui/components/description_step/translations';
|
||||
import type { TiebreakerField as TiebreakerFieldType } from '../../../../../../../../../common/api/detection_engine';
|
||||
|
||||
interface TiebreakerFieldReadOnlyProps {
|
||||
tiebreakerField?: TiebreakerFieldType;
|
||||
}
|
||||
|
||||
export function TiebreakerFieldReadOnly({ tiebreakerField }: TiebreakerFieldReadOnlyProps) {
|
||||
if (!tiebreakerField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiDescriptionList
|
||||
listItems={[
|
||||
{
|
||||
title: descriptionStepI18n.EQL_TIEBREAKER_FIELD_LABEL,
|
||||
description: <TiebreakerField tiebreakerField={tiebreakerField} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface TiebreakerFieldProps {
|
||||
tiebreakerField: TiebreakerFieldType;
|
||||
}
|
||||
|
||||
function TiebreakerField({ tiebreakerField }: TiebreakerFieldProps) {
|
||||
return <EuiText size="s">{tiebreakerField}</EuiText>;
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { Story } from '@storybook/react';
|
||||
import { TimestampFieldReadOnly } from './timestamp_field';
|
||||
import { FieldReadOnly } from '../../field_readonly';
|
||||
import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine';
|
||||
import { mockEqlRule } from '../../storybook/mocks';
|
||||
import { ThreeWayDiffStorybookProviders } from '../../storybook/three_way_diff_storybook_providers';
|
||||
|
||||
export default {
|
||||
component: TimestampFieldReadOnly,
|
||||
title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/timestamp_field',
|
||||
};
|
||||
|
||||
interface TemplateProps {
|
||||
finalDiffableRule: DiffableRule;
|
||||
}
|
||||
|
||||
const Template: Story<TemplateProps> = (args) => {
|
||||
return (
|
||||
<ThreeWayDiffStorybookProviders finalDiffableRule={args.finalDiffableRule}>
|
||||
<FieldReadOnly fieldName="timestamp_field" />
|
||||
</ThreeWayDiffStorybookProviders>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
Default.args = {
|
||||
finalDiffableRule: mockEqlRule({
|
||||
timestamp_field: 'event.created',
|
||||
}),
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiDescriptionList, EuiText } from '@elastic/eui';
|
||||
import * as descriptionStepI18n from '../../../../../../../rule_creation_ui/components/description_step/translations';
|
||||
import type { TimestampField as TimestampFieldType } from '../../../../../../../../../common/api/detection_engine';
|
||||
|
||||
interface TimestampFieldReadOnlyProps {
|
||||
timestampField?: TimestampFieldType;
|
||||
}
|
||||
|
||||
export function TimestampFieldReadOnly({ timestampField }: TimestampFieldReadOnlyProps) {
|
||||
if (!timestampField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiDescriptionList
|
||||
listItems={[
|
||||
{
|
||||
title: descriptionStepI18n.EQL_TIMESTAMP_FIELD_LABEL,
|
||||
description: <TimestampField timestampField={timestampField} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface TimestampFieldProps {
|
||||
timestampField: TimestampFieldType;
|
||||
}
|
||||
|
||||
function TimestampField({ timestampField }: TimestampFieldProps) {
|
||||
return <EuiText size="s">{timestampField}</EuiText>;
|
||||
}
|
|
@ -355,7 +355,6 @@ describe('alert actions', () => {
|
|||
eventCategoryField: 'event.category',
|
||||
query: '',
|
||||
size: 100,
|
||||
tiebreakerField: '',
|
||||
timestampField: '@timestamp',
|
||||
},
|
||||
eventIdToNoteIds: {},
|
||||
|
|
|
@ -9,7 +9,7 @@ import { isEmpty } from 'lodash/fp';
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { EqlOptionsSelected } from '@kbn/timelines-plugin/common';
|
||||
import type { EqlOptions } from '@kbn/timelines-plugin/common';
|
||||
import { convertKueryToElasticSearchQuery } from '../../../../common/lib/kuery';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { useSourcererDataView } from '../../../../sourcerer/containers';
|
||||
|
@ -37,7 +37,7 @@ export type SetRuleQuery = ({
|
|||
}: {
|
||||
index: string[];
|
||||
queryBar: FieldValueQueryBar;
|
||||
eqlOptions?: EqlOptionsSelected;
|
||||
eqlOptions?: EqlOptions;
|
||||
}) => void;
|
||||
|
||||
export const useRuleFromTimeline = (setRuleQuery: SetRuleQuery): RuleFromTimeline => {
|
||||
|
|
|
@ -50,7 +50,7 @@ import type {
|
|||
RequiredFieldInput,
|
||||
} from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import type { SortOrder } from '../../../../../common/api/detection_engine';
|
||||
import type { EqlOptionsSelected } from '../../../../../common/search_strategy';
|
||||
import type { EqlOptions } from '../../../../../common/search_strategy';
|
||||
import type {
|
||||
RuleResponseAction,
|
||||
ResponseAction,
|
||||
|
@ -164,7 +164,7 @@ export interface DefineStepRule {
|
|||
threatIndex: ThreatIndex;
|
||||
threatQueryBar: FieldValueQueryBar;
|
||||
threatMapping: ThreatMapping;
|
||||
eqlOptions: EqlOptionsSelected;
|
||||
eqlOptions: EqlOptions;
|
||||
dataSourceType: DataSourceType;
|
||||
newTermsFields: string[];
|
||||
historyWindowSize: string;
|
||||
|
|
|
@ -5,36 +5,28 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty, isEqual } from 'lodash';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import React, { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { EuiOutsideClickDetector } from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import type {
|
||||
EqlOptionsSelected,
|
||||
FieldsEqlOptions,
|
||||
} from '../../../../../../common/search_strategy';
|
||||
import type { EqlOptions } from '../../../../../../common/search_strategy';
|
||||
import { useSourcererDataView } from '../../../../../sourcerer/containers';
|
||||
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
|
||||
import { EqlQueryBar } from '../../../../../detection_engine/rule_creation_ui/components/eql_query_bar';
|
||||
|
||||
import {
|
||||
debounceAsync,
|
||||
eqlValidator,
|
||||
} from '../../../../../detection_engine/rule_creation_ui/components/eql_query_bar/validators';
|
||||
import { EqlQueryEdit } from '../../../../../detection_engine/rule_creation/components/eql_query_edit';
|
||||
import type { FieldValueQueryBar } from '../../../../../detection_engine/rule_creation_ui/components/query_bar';
|
||||
|
||||
import type { FormSchema } from '../../../../../shared_imports';
|
||||
import { Form, UseField, useForm, useFormData } from '../../../../../shared_imports';
|
||||
import type { FormSchema, FormSubmitHandler } from '../../../../../shared_imports';
|
||||
import { Form, UseField, useForm } from '../../../../../shared_imports';
|
||||
import { timelineActions } from '../../../../store';
|
||||
import * as i18n from '../translations';
|
||||
import { getEqlOptions } from './selectors';
|
||||
|
||||
interface TimelineEqlQueryBar {
|
||||
index: string[];
|
||||
eqlQueryBar: FieldValueQueryBar;
|
||||
eqlOptions: EqlOptionsSelected;
|
||||
eqlOptions: EqlOptions;
|
||||
}
|
||||
|
||||
const defaultValues = {
|
||||
|
@ -55,13 +47,6 @@ const schema: FormSchema<TimelineEqlQueryBar> = {
|
|||
eqlOptions: {
|
||||
fieldsToValidateOnChange: ['eqlOptions', 'eqlQueryBar'],
|
||||
},
|
||||
eqlQueryBar: {
|
||||
validations: [
|
||||
{
|
||||
validator: debounceAsync(eqlValidator, 300),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const hiddenUseFieldClassName = css`
|
||||
|
@ -71,15 +56,12 @@ const hiddenUseFieldClassName = css`
|
|||
// eslint-disable-next-line react/display-name
|
||||
export const EqlQueryBarTimeline = memo(({ timelineId }: { timelineId: string }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isInit = useRef(true);
|
||||
const [isQueryBarValid, setIsQueryBarValid] = useState(false);
|
||||
const [isQueryBarValidating, setIsQueryBarValidating] = useState(false);
|
||||
const getOptionsSelected = useMemo(() => getEqlOptions(), []);
|
||||
const optionsSelected = useDeepEqualSelector((state) => getOptionsSelected(state, timelineId));
|
||||
const eqlOptions = useDeepEqualSelector((state) => getOptionsSelected(state, timelineId));
|
||||
|
||||
const {
|
||||
loading: indexPatternsLoading,
|
||||
indexPattern,
|
||||
sourcererDataView,
|
||||
selectedPatterns,
|
||||
} = useSourcererDataView(SourcererScopeName.timeline);
|
||||
|
||||
|
@ -89,131 +71,117 @@ export const EqlQueryBarTimeline = memo(({ timelineId }: { timelineId: string })
|
|||
index: [...selectedPatterns].sort(),
|
||||
eqlQueryBar: {
|
||||
...defaultValues.eqlQueryBar,
|
||||
query: { query: optionsSelected.query ?? '', language: 'eql' },
|
||||
query: { query: eqlOptions.query ?? '', language: 'eql' },
|
||||
},
|
||||
eqlOptions,
|
||||
}),
|
||||
[optionsSelected.query, selectedPatterns]
|
||||
[eqlOptions, selectedPatterns]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback<FormSubmitHandler<TimelineEqlQueryBar>>(
|
||||
async (formData, isValid) => {
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (eqlOptions.query !== `${formData.eqlQueryBar.query.query}`) {
|
||||
dispatch(
|
||||
timelineActions.updateEqlOptions({
|
||||
id: timelineId,
|
||||
field: 'query',
|
||||
value: `${formData.eqlQueryBar.query.query}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
for (const fieldName of Object.keys(formData.eqlOptions) as Array<
|
||||
keyof typeof formData.eqlOptions
|
||||
>) {
|
||||
if (formData.eqlOptions[fieldName] !== eqlOptions[fieldName]) {
|
||||
dispatch(
|
||||
timelineActions.updateEqlOptions({
|
||||
id: timelineId,
|
||||
field: fieldName,
|
||||
value: formData.eqlOptions[fieldName],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[dispatch, timelineId, eqlOptions]
|
||||
);
|
||||
|
||||
const { form } = useForm<TimelineEqlQueryBar>({
|
||||
defaultValue: initialState,
|
||||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
onSubmit: handleSubmit,
|
||||
});
|
||||
const { getFields, setFieldValue } = form;
|
||||
const { getFields } = form;
|
||||
const handleOutsideEqlQueryEditClick = useCallback(() => form.submit(), [form]);
|
||||
|
||||
const onOptionsChange = useCallback(
|
||||
(field: FieldsEqlOptions, value: string | undefined) => {
|
||||
dispatch(
|
||||
timelineActions.updateEqlOptions({
|
||||
id: timelineId,
|
||||
field,
|
||||
value,
|
||||
})
|
||||
);
|
||||
setFieldValue('eqlOptions', { ...optionsSelected, [field]: value });
|
||||
},
|
||||
[dispatch, optionsSelected, setFieldValue, timelineId]
|
||||
);
|
||||
// Reset the form when new EQL Query came from the state
|
||||
useEffect(() => {
|
||||
getFields().eqlQueryBar.setValue({
|
||||
...defaultValues.eqlQueryBar,
|
||||
query: { query: eqlOptions.query ?? '', language: 'eql' },
|
||||
});
|
||||
}, [getFields, eqlOptions.query]);
|
||||
|
||||
const [{ eqlQueryBar: formEqlQueryBar }] = useFormData<TimelineEqlQueryBar>({
|
||||
form,
|
||||
watch: ['eqlQueryBar'],
|
||||
});
|
||||
|
||||
const prevEqlQuery = useRef<TimelineEqlQueryBar['eqlQueryBar']['query']['query']>('');
|
||||
|
||||
const optionsData = useMemo(
|
||||
() =>
|
||||
isEmpty(indexPattern.fields)
|
||||
? {
|
||||
keywordFields: [],
|
||||
dateFields: [],
|
||||
nonDateFields: [],
|
||||
}
|
||||
: {
|
||||
keywordFields: indexPattern.fields
|
||||
.filter((f) => f.esTypes?.includes('keyword'))
|
||||
.map((f) => ({ label: f.name })),
|
||||
dateFields: indexPattern.fields
|
||||
.filter((f) => f.type === 'date')
|
||||
.map((f) => ({ label: f.name })),
|
||||
nonDateFields: indexPattern.fields
|
||||
.filter((f) => f.type !== 'date')
|
||||
.map((f) => ({ label: f.name })),
|
||||
},
|
||||
[indexPattern]
|
||||
);
|
||||
// Reset the form when new EQL Options came from the state
|
||||
useEffect(() => {
|
||||
getFields().eqlOptions.setValue({
|
||||
eventCategoryField: eqlOptions.eventCategoryField,
|
||||
tiebreakerField: eqlOptions.tiebreakerField,
|
||||
timestampField: eqlOptions.timestampField,
|
||||
size: eqlOptions.size,
|
||||
});
|
||||
}, [
|
||||
getFields,
|
||||
eqlOptions.eventCategoryField,
|
||||
eqlOptions.tiebreakerField,
|
||||
eqlOptions.timestampField,
|
||||
eqlOptions.size,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const { index: indexField } = getFields();
|
||||
const newIndexValue = [...selectedPatterns].sort();
|
||||
const indexFieldValue = (indexField.value as string[]).sort();
|
||||
|
||||
if (!isEqual(indexFieldValue, newIndexValue)) {
|
||||
indexField.setValue(newIndexValue);
|
||||
}
|
||||
}, [getFields, selectedPatterns]);
|
||||
|
||||
useEffect(() => {
|
||||
const { eqlQueryBar } = getFields();
|
||||
if (isInit.current) {
|
||||
isInit.current = false;
|
||||
setIsQueryBarValidating(true);
|
||||
eqlQueryBar.setValue({
|
||||
...defaultValues.eqlQueryBar,
|
||||
query: { query: optionsSelected.query ?? '', language: 'eql' },
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
isInit.current = true;
|
||||
};
|
||||
}, [getFields, optionsSelected.query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
formEqlQueryBar != null &&
|
||||
prevEqlQuery.current !== formEqlQueryBar.query.query &&
|
||||
isQueryBarValid &&
|
||||
!isQueryBarValidating
|
||||
) {
|
||||
prevEqlQuery.current = formEqlQueryBar.query.query;
|
||||
dispatch(
|
||||
timelineActions.updateEqlOptions({
|
||||
id: timelineId,
|
||||
field: 'query',
|
||||
value: `${formEqlQueryBar.query.query}`,
|
||||
})
|
||||
);
|
||||
setIsQueryBarValid(false);
|
||||
setIsQueryBarValidating(false);
|
||||
}
|
||||
}, [dispatch, formEqlQueryBar, isQueryBarValid, isQueryBarValidating, timelineId]);
|
||||
const dataView = useMemo(
|
||||
() => ({
|
||||
...sourcererDataView,
|
||||
title: sourcererDataView?.title ?? '',
|
||||
fields: Object.values(sourcererDataView?.fields || {}),
|
||||
}),
|
||||
[sourcererDataView]
|
||||
);
|
||||
|
||||
/* Force casting `sourcererDataView` to `DataViewBase` is required since EqlQueryEdit
|
||||
accepts DataViewBase but `useSourcererDataView()` returns `DataViewSpec`.
|
||||
|
||||
When using `UseField` with `EqlQueryBar` such casting isn't required by TS since
|
||||
`UseField` component props are types as `Record<string, any>`. */
|
||||
return (
|
||||
<Form form={form} data-test-subj="EqlQueryBarTimeline">
|
||||
<UseField key="Index" path="index" className={hiddenUseFieldClassName} />
|
||||
<UseField key="EqlOptions" path="eqlOptions" className={hiddenUseFieldClassName} />
|
||||
<UseField
|
||||
key="EqlQueryBar"
|
||||
path="eqlQueryBar"
|
||||
component={EqlQueryBar}
|
||||
componentProps={{
|
||||
optionsData,
|
||||
optionsSelected,
|
||||
onOptionsChange,
|
||||
onValidityChange: setIsQueryBarValid,
|
||||
onValiditingChange: setIsQueryBarValidating,
|
||||
idAria: 'timelineEqlQueryBar',
|
||||
isDisabled: indexPatternsLoading,
|
||||
isLoading: indexPatternsLoading,
|
||||
indexPattern,
|
||||
dataTestSubj: 'timelineEqlQueryBar',
|
||||
}}
|
||||
config={{
|
||||
...schema.eqlQueryBar,
|
||||
label: i18n.EQL_QUERY_BAR_LABEL,
|
||||
}}
|
||||
/>
|
||||
<EuiOutsideClickDetector onOutsideClick={handleOutsideEqlQueryEditClick}>
|
||||
<EqlQueryEdit
|
||||
key="EqlQueryBar"
|
||||
path="eqlQueryBar"
|
||||
eqlOptionsPath="eqlOptions"
|
||||
showEqlSizeOption
|
||||
dataView={dataView}
|
||||
loading={indexPatternsLoading}
|
||||
disabled={indexPatternsLoading}
|
||||
/>
|
||||
</EuiOutsideClickDetector>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -13,12 +13,7 @@ export const getEqlOptions = () =>
|
|||
selectTimeline,
|
||||
(timeline) =>
|
||||
timeline?.eqlOptions ?? {
|
||||
eventCategoryField: [{ label: 'event.category' }],
|
||||
tiebreakerField: [
|
||||
{
|
||||
label: '',
|
||||
},
|
||||
],
|
||||
eventCategoryField: 'event.category',
|
||||
timestampField: [
|
||||
{
|
||||
label: '@timestamp',
|
||||
|
|
|
@ -41,7 +41,7 @@ import { TimelineId } from '../../../common/types/timeline';
|
|||
import { useRouteSpy } from '../../common/utils/route/use_route_spy';
|
||||
import { activeTimeline } from './active_timeline_context';
|
||||
import type {
|
||||
EqlOptionsSelected,
|
||||
EqlOptions,
|
||||
TimelineEqlResponse,
|
||||
} from '../../../common/search_strategy/timeline/events/eql';
|
||||
import { useTrackHttpRequest } from '../../common/lib/apm/use_track_http_request';
|
||||
|
@ -84,7 +84,7 @@ type TimelineResponse<T extends KueryFilterQueryKind> = T extends 'kuery'
|
|||
export interface UseTimelineEventsProps {
|
||||
dataViewId: string | null;
|
||||
endDate?: string;
|
||||
eqlOptions?: EqlOptionsSelected;
|
||||
eqlOptions?: EqlOptions;
|
||||
fields: string[];
|
||||
filterQuery?: ESQuery | string;
|
||||
id: string;
|
||||
|
@ -112,7 +112,7 @@ export const initSortDefault: TimelineRequestSortField[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const deStructureEqlOptions = (eqlOptions?: EqlOptionsSelected) => ({
|
||||
const deStructureEqlOptions = (eqlOptions?: EqlOptions) => ({
|
||||
...(!isEmpty(eqlOptions?.eventCategoryField)
|
||||
? {
|
||||
eventCategoryField: eqlOptions?.eventCategoryField,
|
||||
|
|
|
@ -188,7 +188,7 @@ export const toggleModalSaveTimeline = actionCreator<{
|
|||
export const updateEqlOptions = actionCreator<{
|
||||
id: string;
|
||||
field: FieldsEqlOptions;
|
||||
value: string | undefined;
|
||||
value: string | number | undefined;
|
||||
}>('UPDATE_EQL_OPTIONS_TIMELINE');
|
||||
|
||||
export const setEventsLoading = actionCreator<{
|
||||
|
|
|
@ -32,7 +32,6 @@ export const timelineDefaults: SubsetTimelineModel &
|
|||
description: '',
|
||||
eqlOptions: {
|
||||
eventCategoryField: 'event.category',
|
||||
tiebreakerField: '',
|
||||
timestampField: '@timestamp',
|
||||
query: '',
|
||||
size: 100,
|
||||
|
|
|
@ -8,10 +8,7 @@
|
|||
import type { Filter } from '@kbn/es-query';
|
||||
import type { SavedSearch } from '@kbn/saved-search-plugin/common';
|
||||
import type { SessionViewConfig } from '../../../common/types';
|
||||
import type {
|
||||
EqlOptionsSelected,
|
||||
TimelineNonEcsData,
|
||||
} from '../../../common/search_strategy/timeline';
|
||||
import type { EqlOptions, TimelineNonEcsData } from '../../../common/search_strategy/timeline';
|
||||
import type {
|
||||
TimelineTabs,
|
||||
ScrollToTopEvent,
|
||||
|
@ -42,7 +39,7 @@ export interface TimelineModel {
|
|||
createdBy?: string;
|
||||
/** A summary of the events and notes in this timeline */
|
||||
description: string;
|
||||
eqlOptions: EqlOptionsSelected;
|
||||
eqlOptions: EqlOptions;
|
||||
/** Type of event you want to see in this timeline */
|
||||
eventType?: TimelineEventsType;
|
||||
/** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { get } from 'lodash';
|
||||
import { get, has } from 'lodash';
|
||||
import type {
|
||||
RuleSchedule,
|
||||
DataSourceIndexPatterns,
|
||||
|
@ -48,9 +48,13 @@ export const mapDiffableRuleFieldValueToRuleSchemaFormat = (
|
|||
return transformedValue.value;
|
||||
}
|
||||
|
||||
if (!SUBFIELD_MAPPING[fieldName] && !has(diffableField, diffableRuleSubfieldName)) {
|
||||
return diffableField;
|
||||
}
|
||||
|
||||
// From the ThreeWayDiff, get the specific field that maps to the diffable rule field
|
||||
// Otherwise, the diffableField itself already matches the rule field, so retrieve that value.
|
||||
const mappedField = get(diffableField, diffableRuleSubfieldName, diffableField);
|
||||
const mappedField = get(diffableField, diffableRuleSubfieldName);
|
||||
|
||||
return mappedField;
|
||||
};
|
||||
|
@ -81,24 +85,6 @@ export function mapRuleFieldToDiffableRuleField({
|
|||
ruleType,
|
||||
fieldName,
|
||||
}: MapRuleFieldToDiffableRuleFieldParams): keyof AllFieldsDiff {
|
||||
const diffableRuleFieldMap: Record<string, keyof AllFieldsDiff> = {
|
||||
building_block_type: 'building_block',
|
||||
saved_id: 'kql_query',
|
||||
threat_query: 'threat_query',
|
||||
threat_language: 'threat_query',
|
||||
threat_filters: 'threat_query',
|
||||
index: 'data_source',
|
||||
data_view_id: 'data_source',
|
||||
rule_name_override: 'rule_name_override',
|
||||
interval: 'rule_schedule',
|
||||
from: 'rule_schedule',
|
||||
to: 'rule_schedule',
|
||||
timeline_id: 'timeline_template',
|
||||
timeline_title: 'timeline_template',
|
||||
timestamp_override: 'timestamp_override',
|
||||
timestamp_override_fallback_disabled: 'timestamp_override',
|
||||
};
|
||||
|
||||
// Handle query, filters and language fields based on rule type
|
||||
if (fieldName === 'query' || fieldName === 'language' || fieldName === 'filters') {
|
||||
switch (ruleType) {
|
||||
|
@ -114,9 +100,48 @@ export function mapRuleFieldToDiffableRuleField({
|
|||
}
|
||||
}
|
||||
|
||||
const diffableRuleFieldMap: Record<string, keyof AllFieldsDiff> = {
|
||||
building_block_type: 'building_block',
|
||||
saved_id: 'kql_query',
|
||||
event_category_override: 'eql_query',
|
||||
tiebreaker_field: 'eql_query',
|
||||
timestamp_field: 'eql_query',
|
||||
threat_query: 'threat_query',
|
||||
threat_language: 'threat_query',
|
||||
threat_filters: 'threat_query',
|
||||
index: 'data_source',
|
||||
data_view_id: 'data_source',
|
||||
rule_name_override: 'rule_name_override',
|
||||
interval: 'rule_schedule',
|
||||
from: 'rule_schedule',
|
||||
to: 'rule_schedule',
|
||||
timeline_id: 'timeline_template',
|
||||
timeline_title: 'timeline_template',
|
||||
timestamp_override: 'timestamp_override',
|
||||
timestamp_override_fallback_disabled: 'timestamp_override',
|
||||
};
|
||||
|
||||
return diffableRuleFieldMap[fieldName] || fieldName;
|
||||
}
|
||||
|
||||
const SUBFIELD_MAPPING: Record<string, string> = {
|
||||
index: 'index_patterns',
|
||||
data_view_id: 'data_view_id',
|
||||
saved_id: 'saved_query_id',
|
||||
event_category_override: 'event_category_override',
|
||||
tiebreaker_field: 'tiebreaker_field',
|
||||
timestamp_field: 'timestamp_field',
|
||||
building_block_type: 'type',
|
||||
rule_name_override: 'field_name',
|
||||
timestamp_override: 'field_name',
|
||||
timestamp_override_fallback_disabled: 'fallback_disabled',
|
||||
timeline_id: 'timeline_id',
|
||||
timeline_title: 'timeline_title',
|
||||
interval: 'interval',
|
||||
from: 'lookback',
|
||||
to: 'lookback',
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps a PrebuiltRuleAsset schema field name to its corresponding property
|
||||
* name within a DiffableRule group.
|
||||
|
@ -134,22 +159,7 @@ export function mapRuleFieldToDiffableRuleField({
|
|||
*
|
||||
*/
|
||||
export function mapRuleFieldToDiffableRuleSubfield(fieldName: string): string {
|
||||
const fieldMapping: Record<string, string> = {
|
||||
index: 'index_patterns',
|
||||
data_view_id: 'data_view_id',
|
||||
saved_id: 'saved_query_id',
|
||||
building_block_type: 'type',
|
||||
rule_name_override: 'field_name',
|
||||
timestamp_override: 'field_name',
|
||||
timestamp_override_fallback_disabled: 'fallback_disabled',
|
||||
timeline_id: 'timeline_id',
|
||||
timeline_title: 'timeline_title',
|
||||
interval: 'interval',
|
||||
from: 'lookback',
|
||||
to: 'lookback',
|
||||
};
|
||||
|
||||
return fieldMapping[fieldName] || fieldName;
|
||||
return SUBFIELD_MAPPING[fieldName] || fieldName;
|
||||
}
|
||||
|
||||
type TransformValuesReturnType =
|
||||
|
|
|
@ -239,9 +239,6 @@ const eqlFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor<DiffableEqlFields> = {
|
|||
type: ruleTypeDiffAlgorithm,
|
||||
eql_query: eqlQueryDiffAlgorithm,
|
||||
data_source: dataSourceDiffAlgorithm,
|
||||
event_category_override: singleLineStringDiffAlgorithm,
|
||||
timestamp_field: singleLineStringDiffAlgorithm,
|
||||
tiebreaker_field: singleLineStringDiffAlgorithm,
|
||||
alert_suppression: simpleDiffAlgorithm,
|
||||
};
|
||||
|
||||
|
|
|
@ -42,8 +42,8 @@ export type {
|
|||
BeatFields,
|
||||
BrowserFields,
|
||||
CursorType,
|
||||
EqlOptionsData,
|
||||
EqlOptionsSelected,
|
||||
EqlFieldsComboBoxOptions,
|
||||
EqlOptions,
|
||||
FieldsEqlOptions,
|
||||
FieldInfo,
|
||||
IndexField,
|
||||
|
|
|
@ -22,13 +22,13 @@ export interface TimelineEqlResponse extends EqlSearchStrategyResponse<EqlSearch
|
|||
inspect: Maybe<Inspect>;
|
||||
}
|
||||
|
||||
export interface EqlOptionsData {
|
||||
export interface EqlFieldsComboBoxOptions {
|
||||
keywordFields: EuiComboBoxOptionOption[];
|
||||
dateFields: EuiComboBoxOptionOption[];
|
||||
nonDateFields: EuiComboBoxOptionOption[];
|
||||
}
|
||||
|
||||
export interface EqlOptionsSelected {
|
||||
export interface EqlOptions {
|
||||
eventCategoryField?: string;
|
||||
tiebreakerField?: string;
|
||||
timestampField?: string;
|
||||
|
@ -36,4 +36,4 @@ export interface EqlOptionsSelected {
|
|||
size?: number;
|
||||
}
|
||||
|
||||
export type FieldsEqlOptions = keyof EqlOptionsSelected;
|
||||
export type FieldsEqlOptions = keyof EqlOptions;
|
||||
|
|
|
@ -38124,7 +38124,6 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlInfoAriaLabel": "Ouvrir une fenêtre contextuelle d'aide",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlInfoTooltipContent": "Consultez {createEsqlRuleTypeLink} pour commencer à utiliser les règles ES|QL.",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlInfoTooltipLink": "documentation",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlQueryFieldRequiredError": "Une requête ES|QL est requise.",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlQueryLabel": "Requête ES|QL",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel": "Seuil de score d'anomalie",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel": "Tâche de Machine Learning",
|
||||
|
|
|
@ -38091,7 +38091,6 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlInfoAriaLabel": "ヘルプポップオーバーを開く",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlInfoTooltipContent": "ES|QL ルールの使用を開始するには、{createEsqlRuleTypeLink}を確認してください。",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlInfoTooltipLink": "ドキュメンテーション",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlQueryFieldRequiredError": "ES|QLクエリは必須です。",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlQueryLabel": "ES|QLクエリ",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel": "異常スコアしきい値",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel": "機械学習ジョブ",
|
||||
|
|
|
@ -38158,7 +38158,6 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlInfoAriaLabel": "打开帮助弹出框",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlInfoTooltipContent": "请访问我们的{createEsqlRuleTypeLink}以开始使用 ES|QL 规则。",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlInfoTooltipLink": "文档",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlQueryFieldRequiredError": "ES|QL 查询必填。",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.esqlQueryLabel": "ES|QL 查询",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel": "异常分数阈值",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel": "Machine Learning 作业",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue