diff --git a/config/serverless.security.yml b/config/serverless.security.yml index 5ebb22486dc43..88770178a3493 100644 --- a/config/serverless.security.yml +++ b/config/serverless.security.yml @@ -23,7 +23,6 @@ xpack.securitySolutionServerless.productTypes: xpack.securitySolution.offeringSettings: { ILMEnabled: false, # Index Lifecycle Management (ILM) functionalities disabled, not supported by serverless Elasticsearch - ESQLEnabled: false, # ES|QL disabled, not supported by serverless Elasticsearch } newsfeed.enabled: true diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 1e2097639cd30..e821cb98f4fd0 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -82,8 +82,9 @@ Fleet:: * Fixes managed agent policy preconfiguration update ({kibana-pull}181624[#181624]). * Use lowercase dataset in template names ({kibana-pull}180887[#180887]). * Fixes KQL/kuery for getting Fleet Server agent count ({kibana-pull}180650[#180650]). +Index Management:: +* Fixes `allow_auto_create` field in the Index Template form ({kibana-pull}178321[#178321]). Lens & Visualizations:: -* Fixes table sorting on time picker interval change in *Lens* ({kibana-pull}182173[#182173]). * Fixes controls on fields with custom label ({kibana-pull}180615[#180615]). Machine Learning:: * Fixes deep link for Index data visualizer & ES|QL data visualizer ({kibana-pull}180389[#180389]). diff --git a/fleet_packages.json b/fleet_packages.json index 317c4f1d60043..15a653900f8b4 100644 --- a/fleet_packages.json +++ b/fleet_packages.json @@ -30,7 +30,7 @@ }, { "name": "elastic_agent", - "version": "1.18.0" + "version": "1.19.0" }, { "name": "endpoint", diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts index 7844319de1df9..f1469fa57ced6 100644 --- a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts +++ b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts @@ -367,6 +367,7 @@ export function createPluginStartContext({ }, security: { authc: deps.security.authc, + audit: deps.security.audit, }, userProfile: deps.userProfile, }; diff --git a/packages/core/security/core-security-server-internal/src/security_route_handler_context.ts b/packages/core/security/core-security-server-internal/src/security_route_handler_context.ts index 451b0b2aa1114..4fa328782dd0e 100644 --- a/packages/core/security/core-security-server-internal/src/security_route_handler_context.ts +++ b/packages/core/security/core-security-server-internal/src/security_route_handler_context.ts @@ -10,12 +10,13 @@ import type { KibanaRequest } from '@kbn/core-http-server'; import type { SecurityRequestHandlerContext, AuthcRequestHandlerContext, + AuditRequestHandlerContext, } from '@kbn/core-security-server'; import type { InternalSecurityServiceStart } from './internal_contracts'; export class CoreSecurityRouteHandlerContext implements SecurityRequestHandlerContext { #authc?: AuthcRequestHandlerContext; - + #audit?: AuditRequestHandlerContext; constructor( private readonly securityStart: InternalSecurityServiceStart, private readonly request: KibanaRequest @@ -29,4 +30,13 @@ export class CoreSecurityRouteHandlerContext implements SecurityRequestHandlerCo } return this.#authc; } + + public get audit() { + if (this.#audit == null) { + this.#audit = { + logger: this.securityStart.audit.asScoped(this.request), + }; + } + return this.#audit; + } } diff --git a/packages/core/security/core-security-server-internal/src/test_helpers/create_audit_logger.mock.ts b/packages/core/security/core-security-server-internal/src/test_helpers/create_audit_logger.mock.ts new file mode 100644 index 0000000000000..b8327c8cee59a --- /dev/null +++ b/packages/core/security/core-security-server-internal/src/test_helpers/create_audit_logger.mock.ts @@ -0,0 +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 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 or the Server + * Side Public License, v 1. + */ + +import { AuditLogger } from '@kbn/core-security-server'; + +export type MockedAuditLogger = jest.Mocked; + +export const createAuditLoggerMock = { + create(): MockedAuditLogger { + return { + log: jest.fn(), + enabled: true, + }; + }, +}; diff --git a/packages/core/security/core-security-server-internal/src/utils/convert_security_api.test.ts b/packages/core/security/core-security-server-internal/src/utils/convert_security_api.test.ts index 6fe51b6873862..7c2e49092f73e 100644 --- a/packages/core/security/core-security-server-internal/src/utils/convert_security_api.test.ts +++ b/packages/core/security/core-security-server-internal/src/utils/convert_security_api.test.ts @@ -8,11 +8,22 @@ import type { CoreSecurityDelegateContract } from '@kbn/core-security-server'; import { convertSecurityApi } from './convert_security_api'; +import { createAuditLoggerMock } from '../test_helpers/create_audit_logger.mock'; describe('convertSecurityApi', () => { it('returns the API from the source', () => { - const source: CoreSecurityDelegateContract = { authc: { getCurrentUser: jest.fn() } }; + const source: CoreSecurityDelegateContract = { + authc: { + getCurrentUser: jest.fn(), + }, + audit: { + asScoped: jest.fn().mockReturnValue(createAuditLoggerMock.create()), + withoutRequest: createAuditLoggerMock.create(), + }, + }; const output = convertSecurityApi(source); expect(output.authc.getCurrentUser).toBe(source.authc.getCurrentUser); + expect(output.audit.asScoped).toBe(source.audit.asScoped); + expect(output.audit.withoutRequest).toBe(source.audit.withoutRequest); }); }); diff --git a/packages/core/security/core-security-server-internal/src/utils/default_implementation.test.ts b/packages/core/security/core-security-server-internal/src/utils/default_implementation.test.ts index 17393d5994bf1..e4348404671b9 100644 --- a/packages/core/security/core-security-server-internal/src/utils/default_implementation.test.ts +++ b/packages/core/security/core-security-server-internal/src/utils/default_implementation.test.ts @@ -22,4 +22,19 @@ describe('getDefaultSecurityImplementation', () => { expect(user).toBeNull(); }); }); + + describe('audit.asScoped', () => { + it('returns null', async () => { + const logger = implementation.audit.asScoped({} as any); + expect(logger.log({ message: 'something' })).toBeUndefined(); + }); + }); + + describe('audit.withoutRequest', () => { + it('does not log', async () => { + const logger = implementation.audit.withoutRequest; + expect(logger.enabled).toBe(false); + expect(logger.log({ message: 'no request' })).toBeUndefined(); + }); + }); }); diff --git a/packages/core/security/core-security-server-internal/src/utils/default_implementation.ts b/packages/core/security/core-security-server-internal/src/utils/default_implementation.ts index bd4ce287fd498..91819807f1064 100644 --- a/packages/core/security/core-security-server-internal/src/utils/default_implementation.ts +++ b/packages/core/security/core-security-server-internal/src/utils/default_implementation.ts @@ -13,5 +13,14 @@ export const getDefaultSecurityImplementation = (): CoreSecurityDelegateContract authc: { getCurrentUser: () => null, }, + audit: { + asScoped: () => { + return { log: () => undefined, enabled: false }; + }, + withoutRequest: { + log: () => undefined, + enabled: false, + }, + }, }; }; diff --git a/packages/core/security/core-security-server-mocks/index.ts b/packages/core/security/core-security-server-mocks/index.ts index 0e6eafac658e8..23c49282252f0 100644 --- a/packages/core/security/core-security-server-mocks/index.ts +++ b/packages/core/security/core-security-server-mocks/index.ts @@ -7,3 +7,5 @@ */ export { securityServiceMock } from './src/security_service.mock'; +export type { InternalSecurityStartMock, SecurityStartMock } from './src/security_service.mock'; +export { auditLoggerMock } from './src/audit.mock'; diff --git a/packages/core/security/core-security-server-mocks/src/audit.mock.ts b/packages/core/security/core-security-server-mocks/src/audit.mock.ts new file mode 100644 index 0000000000000..c5c117b6189d6 --- /dev/null +++ b/packages/core/security/core-security-server-mocks/src/audit.mock.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 or the Server + * Side Public License, v 1. + */ + +import type { KibanaRequest } from '@kbn/core-http-server'; +import type { AuditLogger } from '@kbn/core-security-server'; + +export type MockedAuditLogger = jest.Mocked; + +export const auditLoggerMock = { + create(): MockedAuditLogger { + return { + log: jest.fn(), + enabled: true, + }; + }, +}; + +export interface MockedAuditService { + asScoped: (request: KibanaRequest) => MockedAuditLogger; + withoutRequest: MockedAuditLogger; +} + +export const auditServiceMock = { + create(): MockedAuditService { + return { + asScoped: jest.fn().mockReturnValue(auditLoggerMock.create()), + withoutRequest: auditLoggerMock.create(), + }; + }, +}; diff --git a/packages/core/security/core-security-server-mocks/src/security_service.mock.ts b/packages/core/security/core-security-server-mocks/src/security_service.mock.ts index 99f86c84461f3..b19539fd862c0 100644 --- a/packages/core/security/core-security-server-mocks/src/security_service.mock.ts +++ b/packages/core/security/core-security-server-mocks/src/security_service.mock.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { +import { SecurityServiceSetup, SecurityServiceStart, SecurityRequestHandlerContext, @@ -15,6 +15,7 @@ import type { InternalSecurityServiceSetup, InternalSecurityServiceStart, } from '@kbn/core-security-server-internal'; +import { auditServiceMock, type MockedAuditService } from './audit.mock'; const createSetupMock = () => { const mock: jest.Mocked = { @@ -24,11 +25,16 @@ const createSetupMock = () => { return mock; }; -const createStartMock = () => { - const mock: jest.MockedObjectDeep = { +export type SecurityStartMock = jest.MockedObjectDeep> & { + audit: MockedAuditService; +}; + +const createStartMock = (): SecurityStartMock => { + const mock = { authc: { getCurrentUser: jest.fn(), }, + audit: auditServiceMock.create(), }; return mock; @@ -42,11 +48,18 @@ const createInternalSetupMock = () => { return mock; }; -const createInternalStartMock = () => { - const mock: jest.MockedObjectDeep = { +export type InternalSecurityStartMock = jest.MockedObjectDeep< + Omit +> & { + audit: MockedAuditService; +}; + +const createInternalStartMock = (): InternalSecurityStartMock => { + const mock = { authc: { getCurrentUser: jest.fn(), }, + audit: auditServiceMock.create(), }; return mock; @@ -67,6 +80,12 @@ const createRequestHandlerContextMock = () => { authc: { getCurrentUser: jest.fn(), }, + audit: { + logger: { + log: jest.fn(), + enabled: true, + }, + }, }; return mock; }; diff --git a/packages/core/security/core-security-server-mocks/tsconfig.json b/packages/core/security/core-security-server-mocks/tsconfig.json index ca806dd4d5029..28181e131badd 100644 --- a/packages/core/security/core-security-server-mocks/tsconfig.json +++ b/packages/core/security/core-security-server-mocks/tsconfig.json @@ -16,5 +16,6 @@ "kbn_references": [ "@kbn/core-security-server", "@kbn/core-security-server-internal", + "@kbn/core-http-server", ] } diff --git a/packages/core/security/core-security-server/index.ts b/packages/core/security/core-security-server/index.ts index c8dd3efda695c..a4d3027c97fdb 100644 --- a/packages/core/security/core-security-server/index.ts +++ b/packages/core/security/core-security-server/index.ts @@ -8,11 +8,21 @@ export type { SecurityServiceSetup, SecurityServiceStart } from './src/contracts'; export type { CoreAuthenticationService } from './src/authc'; +export type { CoreAuditService } from './src/audit'; export type { CoreSecurityDelegateContract, AuthenticationServiceContract, + AuditServiceContract, } from './src/api_provider'; export type { SecurityRequestHandlerContext, AuthcRequestHandlerContext, + AuditRequestHandlerContext, } from './src/request_handler_context'; +export type { + AuditEvent, + AuditHttp, + AuditKibana, + AuditRequest, +} from './src/audit_logging/audit_events'; +export type { AuditLogger } from './src/audit_logging/audit_logger'; diff --git a/packages/core/security/core-security-server/src/api_provider.ts b/packages/core/security/core-security-server/src/api_provider.ts index 2bcd9bd9b2b97..102c1a0a899c7 100644 --- a/packages/core/security/core-security-server/src/api_provider.ts +++ b/packages/core/security/core-security-server/src/api_provider.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { CoreAuditService } from './audit'; import type { CoreAuthenticationService } from './authc'; /** @@ -16,9 +17,12 @@ import type { CoreAuthenticationService } from './authc'; */ export interface CoreSecurityDelegateContract { authc: AuthenticationServiceContract; + audit: AuditServiceContract; } /** * @public */ export type AuthenticationServiceContract = CoreAuthenticationService; + +export type AuditServiceContract = CoreAuditService; diff --git a/packages/core/security/core-security-server/src/audit.ts b/packages/core/security/core-security-server/src/audit.ts new file mode 100644 index 0000000000000..57d72366fdac8 --- /dev/null +++ b/packages/core/security/core-security-server/src/audit.ts @@ -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 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 or the Server + * Side Public License, v 1. + */ + +import type { KibanaRequest } from '@kbn/core-http-server'; + +import type { AuditLogger } from './audit_logging/audit_logger'; + +export interface CoreAuditService { + /** + * Creates an {@link AuditLogger} scoped to the current request. + * + * This audit logger logs events with all required user and session info and should be used for + * all user-initiated actions. + * + * @example + * ```typescript + * const auditLogger = securitySetup.audit.asScoped(request); + * auditLogger.log(event); + * ``` + */ + asScoped: (request: KibanaRequest) => AuditLogger; + + /** + * {@link AuditLogger} for background tasks only. + * + * This audit logger logs events without any user or session info and should never be used to log + * user-initiated actions. + * + * @example + * ```typescript + * securitySetup.audit.withoutRequest.log(event); + * ``` + */ + withoutRequest: AuditLogger; +} diff --git a/x-pack/packages/security/plugin_types_server/src/audit/audit_events.ts b/packages/core/security/core-security-server/src/audit_logging/audit_events.ts similarity index 91% rename from x-pack/packages/security/plugin_types_server/src/audit/audit_events.ts rename to packages/core/security/core-security-server/src/audit_logging/audit_events.ts index 35d7bb254fc22..f0e9829bba3fc 100644 --- a/x-pack/packages/security/plugin_types_server/src/audit/audit_events.ts +++ b/packages/core/security/core-security-server/src/audit_logging/audit_events.ts @@ -1,11 +1,12 @@ /* * 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. + * 2.0 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 or the Server + * Side Public License, v 1. */ -import type { LogMeta } from '@kbn/core/server'; +import type { LogMeta } from '@kbn/logging'; /** * Audit kibana schema using ECS format diff --git a/x-pack/packages/security/plugin_types_server/src/audit/audit_logger.ts b/packages/core/security/core-security-server/src/audit_logging/audit_logger.ts similarity index 86% rename from x-pack/packages/security/plugin_types_server/src/audit/audit_logger.ts rename to packages/core/security/core-security-server/src/audit_logging/audit_logger.ts index 4670de3aa8d3b..803a167423a29 100644 --- a/x-pack/packages/security/plugin_types_server/src/audit/audit_logger.ts +++ b/packages/core/security/core-security-server/src/audit_logging/audit_logger.ts @@ -1,8 +1,9 @@ /* * 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. + * 2.0 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 or the Server + * Side Public License, v 1. */ import type { AuditEvent } from './audit_events'; diff --git a/packages/core/security/core-security-server/src/contracts.ts b/packages/core/security/core-security-server/src/contracts.ts index 8c75b352c5556..ed25737823f7b 100644 --- a/packages/core/security/core-security-server/src/contracts.ts +++ b/packages/core/security/core-security-server/src/contracts.ts @@ -8,7 +8,7 @@ import type { CoreAuthenticationService } from './authc'; import type { CoreSecurityDelegateContract } from './api_provider'; - +import type { CoreAuditService } from './audit'; /** * Setup contract for Core's security service. * @@ -33,4 +33,8 @@ export interface SecurityServiceStart { * The {@link CoreAuthenticationService | authentication service} */ authc: CoreAuthenticationService; + /** + * The {@link CoreAuditService | audit service} + */ + audit: CoreAuditService; } diff --git a/packages/core/security/core-security-server/src/request_handler_context.ts b/packages/core/security/core-security-server/src/request_handler_context.ts index 6433ea9a919e1..37915c24ddaa1 100644 --- a/packages/core/security/core-security-server/src/request_handler_context.ts +++ b/packages/core/security/core-security-server/src/request_handler_context.ts @@ -7,11 +7,17 @@ */ import type { AuthenticatedUser } from '@kbn/core-security-common'; +import { AuditLogger } from './audit_logging/audit_logger'; export interface SecurityRequestHandlerContext { authc: AuthcRequestHandlerContext; + audit: AuditRequestHandlerContext; } export interface AuthcRequestHandlerContext { getCurrentUser(): AuthenticatedUser | null; } + +export interface AuditRequestHandlerContext { + logger: AuditLogger; +} diff --git a/packages/core/security/core-security-server/tsconfig.json b/packages/core/security/core-security-server/tsconfig.json index 7b6c07b4a6eba..0304c8ef6dee2 100644 --- a/packages/core/security/core-security-server/tsconfig.json +++ b/packages/core/security/core-security-server/tsconfig.json @@ -16,5 +16,6 @@ "kbn_references": [ "@kbn/core-security-common", "@kbn/core-http-server", + "@kbn/logging", ] } diff --git a/packages/shared-ux/markdown/impl/markdown.tsx b/packages/shared-ux/markdown/impl/markdown.tsx index f7993658d77c3..78bfc230afef6 100644 --- a/packages/shared-ux/markdown/impl/markdown.tsx +++ b/packages/shared-ux/markdown/impl/markdown.tsx @@ -88,6 +88,7 @@ export const Markdown = ({ return ( { id: 'FmZBc2NuYlhsU1JxSk5LZXNRczVxdEEed3l6LUVycTVTVGl1LWtDSVdta2VkQToxODUzODUx', rawResponse: { took: 4414, - timed_out: false, + timed_out: true, terminated_early: false, num_reduce_phases: 2, _shards: { diff --git a/src/plugins/data/public/search/search_interceptor/to_partial_response.ts b/src/plugins/data/public/search/search_interceptor/to_partial_response.ts index 8c6af82bb31e4..edba343c7ae4c 100644 --- a/src/plugins/data/public/search/search_interceptor/to_partial_response.ts +++ b/src/plugins/data/public/search/search_interceptor/to_partial_response.ts @@ -47,6 +47,7 @@ export function toPartialResponseAfterTimeout(response: IEsSearchResponse): IEsS isRunning: false, rawResponse: { ...rawResponse, + timed_out: true, _clusters: { ...clusters, details, diff --git a/x-pack/packages/security/plugin_types_server/src/audit/audit_service.ts b/x-pack/packages/security/plugin_types_server/src/audit/audit_service.ts index 88b25b5181a42..e7b7f27b73b07 100644 --- a/x-pack/packages/security/plugin_types_server/src/audit/audit_service.ts +++ b/x-pack/packages/security/plugin_types_server/src/audit/audit_service.ts @@ -7,7 +7,7 @@ import type { KibanaRequest } from '@kbn/core/server'; -import type { AuditLogger } from './audit_logger'; +import type { AuditLogger } from '@kbn/core-security-server'; export interface AuditServiceSetup { /** diff --git a/x-pack/packages/security/plugin_types_server/src/audit/index.ts b/x-pack/packages/security/plugin_types_server/src/audit/index.ts index 0111172cd409f..2545d064d7abe 100644 --- a/x-pack/packages/security/plugin_types_server/src/audit/index.ts +++ b/x-pack/packages/security/plugin_types_server/src/audit/index.ts @@ -6,5 +6,10 @@ */ export type { AuditServiceSetup } from './audit_service'; -export type { AuditEvent, AuditHttp, AuditKibana, AuditRequest } from './audit_events'; -export type { AuditLogger } from './audit_logger'; +export type { + AuditEvent, + AuditHttp, + AuditKibana, + AuditLogger, + AuditRequest, +} from '@kbn/core-security-server'; diff --git a/x-pack/packages/security/plugin_types_server/tsconfig.json b/x-pack/packages/security/plugin_types_server/tsconfig.json index 0edcc935ca144..51a1cf53c62bf 100644 --- a/x-pack/packages/security/plugin_types_server/tsconfig.json +++ b/x-pack/packages/security/plugin_types_server/tsconfig.json @@ -14,5 +14,6 @@ "@kbn/core", "@kbn/security-plugin-types-common", "@kbn/core-user-profile-server", + "@kbn/core-security-server", ] } diff --git a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/empty_state.tsx b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/empty_state.tsx index 449fb1ef1c94d..84f918a2cd0b5 100644 --- a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/empty_state.tsx +++ b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/empty_state.tsx @@ -31,7 +31,7 @@ export const EmptyState: React.FC = () => { -

+

0 ? 1 : 0", + }, + }, + "slo.numerator": Object { + "bucket_script": Object { + "buckets_path": Object { + "numerator": "_numerator['target']>_count", + }, + "script": "params.numerator", + }, + }, + }, + "group_by": Object { + "@timestamp": Object { + "date_histogram": Object { + "field": "@timestamp", + "fixed_interval": "2m", + }, + }, + "service.environment": Object { + "terms": Object { + "field": "service.environment", + }, + }, + "service.name": Object { + "terms": Object { + "field": "service.name", + }, + }, + "slo.id": Object { + "terms": Object { + "field": "slo.id", + }, + }, + "slo.revision": Object { + "terms": Object { + "field": "slo.revision", + }, + }, + "transaction.name": Object { + "terms": Object { + "field": "transaction.name", + }, + }, + "transaction.type": Object { + "terms": Object { + "field": "transaction.type", + }, + }, + }, + }, + "settings": Object { + "deduce_mappings": false, + "unattended": true, + }, + "source": Object { + "index": "metrics-apm*", + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "metric", + ], + }, + }, + Object { + "term": Object { + "metricset.name": "transaction", + }, + }, + Object { + "exists": Object { + "field": "transaction.duration.histogram", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-7d/d", + }, + }, + }, + Object { + "match": Object { + "service.name": "irrelevant", + }, + }, + Object { + "match": Object { + "service.environment": "irrelevant", + }, + }, + Object { + "match": Object { + "transaction.name": "irrelevant", + }, + }, + Object { + "match": Object { + "transaction.type": "irrelevant", + }, + }, + ], + }, + }, + "runtime_mappings": Object { + "slo.id": Object { + "script": Object { + "source": "emit('irrelevant')", + }, + "type": "keyword", + }, + "slo.revision": Object { + "script": Object { + "source": "emit(1)", + }, + "type": "long", + }, + }, + }, + "sync": Object { + "time": Object { + "delay": "1m", + "field": "@timestamp", + }, + }, + "transform_id": "slo-irrelevant-1", +} +`; + exports[`APM Transaction Duration Transform Generator returns the expected transform params with every specified indicator params 1`] = ` Object { "_meta": Object { diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap index a733614c921ce..c5021f57229f0 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/apm_transaction_error_rate.test.ts.snap @@ -529,6 +529,164 @@ Object { } `; +exports[`APM Transaction Error Rate Transform Generator returns the expected transform params for timeslices slo using timesliceTarget = 0 1`] = ` +Object { + "_meta": Object { + "managed": true, + "managed_by": "observability", + "version": 3.2, + }, + "defer_validation": true, + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", + "dest": Object { + "index": ".slo-observability.sli-v3.2", + "pipeline": ".slo-observability.sli.pipeline-v3.2", + }, + "frequency": "1m", + "pivot": Object { + "aggregations": Object { + "slo.denominator": Object { + "filter": Object { + "match_all": Object {}, + }, + }, + "slo.isGoodSlice": Object { + "bucket_script": Object { + "buckets_path": Object { + "goodEvents": "slo.numerator>_count", + "totalEvents": "slo.denominator>_count", + }, + "script": "params.goodEvents / params.totalEvents > 0 ? 1 : 0", + }, + }, + "slo.numerator": Object { + "filter": Object { + "bool": Object { + "should": Object { + "match": Object { + "event.outcome": "success", + }, + }, + }, + }, + }, + }, + "group_by": Object { + "@timestamp": Object { + "date_histogram": Object { + "field": "@timestamp", + "fixed_interval": "2m", + }, + }, + "service.environment": Object { + "terms": Object { + "field": "service.environment", + }, + }, + "service.name": Object { + "terms": Object { + "field": "service.name", + }, + }, + "slo.id": Object { + "terms": Object { + "field": "slo.id", + }, + }, + "slo.revision": Object { + "terms": Object { + "field": "slo.revision", + }, + }, + "transaction.name": Object { + "terms": Object { + "field": "transaction.name", + }, + }, + "transaction.type": Object { + "terms": Object { + "field": "transaction.type", + }, + }, + }, + }, + "settings": Object { + "deduce_mappings": false, + "unattended": true, + }, + "source": Object { + "index": "metrics-apm*", + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "metricset.name": "transaction", + }, + }, + Object { + "terms": Object { + "event.outcome": Array [ + "success", + "failure", + ], + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-7d/d", + }, + }, + }, + Object { + "match": Object { + "service.name": "irrelevant", + }, + }, + Object { + "match": Object { + "service.environment": "irrelevant", + }, + }, + Object { + "match": Object { + "transaction.name": "irrelevant", + }, + }, + Object { + "match": Object { + "transaction.type": "irrelevant", + }, + }, + ], + }, + }, + "runtime_mappings": Object { + "slo.id": Object { + "script": Object { + "source": "emit('irrelevant')", + }, + "type": "keyword", + }, + "slo.revision": Object { + "script": Object { + "source": "emit(1)", + }, + "type": "long", + }, + }, + }, + "sync": Object { + "time": Object { + "delay": "1m", + "field": "@timestamp", + }, + }, + "transform_id": "slo-irrelevant-1", +} +`; + exports[`APM Transaction Error Rate Transform Generator returns the expected transform params with every specified indicator params 1`] = ` Object { "_meta": Object { diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/histogram.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/histogram.test.ts.snap index aba4f77e81465..f9015e64984d9 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/histogram.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/histogram.test.ts.snap @@ -224,6 +224,158 @@ Object { } `; +exports[`Histogram Transform Generator returns the expected transform params for timeslices slo using timesliceTarget = 0 1`] = ` +Object { + "_meta": Object { + "managed": true, + "managed_by": "observability", + "version": 3.2, + }, + "defer_validation": true, + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", + "dest": Object { + "index": ".slo-observability.sli-v3.2", + "pipeline": ".slo-observability.sli.pipeline-v3.2", + }, + "frequency": "1m", + "pivot": Object { + "aggregations": Object { + "_good": Object { + "aggs": Object { + "total": Object { + "range": Object { + "field": "latency", + "keyed": true, + "ranges": Array [ + Object { + "from": 0, + "key": "target", + "to": 100, + }, + ], + }, + }, + }, + "filter": Object { + "match_all": Object {}, + }, + }, + "_total": Object { + "aggs": Object { + "total": Object { + "value_count": Object { + "field": "latency", + }, + }, + }, + "filter": Object { + "match_all": Object {}, + }, + }, + "slo.denominator": Object { + "bucket_script": Object { + "buckets_path": Object { + "value": "_total>total", + }, + "script": "params.value", + }, + }, + "slo.isGoodSlice": Object { + "bucket_script": Object { + "buckets_path": Object { + "goodEvents": "slo.numerator>value", + "totalEvents": "slo.denominator>value", + }, + "script": "params.goodEvents / params.totalEvents > 0 ? 1 : 0", + }, + }, + "slo.numerator": Object { + "bucket_script": Object { + "buckets_path": Object { + "value": "_good>total['target']>_count", + }, + "script": "params.value", + }, + }, + }, + "group_by": Object { + "@timestamp": Object { + "date_histogram": Object { + "field": "log_timestamp", + "fixed_interval": "2m", + }, + }, + "slo.id": Object { + "terms": Object { + "field": "slo.id", + }, + }, + "slo.revision": Object { + "terms": Object { + "field": "slo.revision", + }, + }, + }, + }, + "settings": Object { + "deduce_mappings": false, + "unattended": true, + }, + "source": Object { + "index": Array [ + "my-index*", + "my-other-index*", + ], + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "log_timestamp": Object { + "gte": "now-7d/d", + }, + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "labels.groupId": "group-3", + }, + }, + ], + }, + }, + ], + }, + }, + "runtime_mappings": Object { + "slo.id": Object { + "script": Object { + "source": "emit('irrelevant')", + }, + "type": "keyword", + }, + "slo.revision": Object { + "script": Object { + "source": "emit(1)", + }, + "type": "long", + }, + }, + }, + "sync": Object { + "time": Object { + "delay": "1m", + "field": "log_timestamp", + }, + }, + "transform_id": "slo-irrelevant-1", +} +`; + exports[`Histogram Transform Generator returns the expected transform params with every specified indicator params 1`] = ` Object { "_meta": Object { diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/kql_custom.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/kql_custom.test.ts.snap index bf5a09bf6b6f2..b85642d631f26 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/kql_custom.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/__snapshots__/kql_custom.test.ts.snap @@ -238,6 +238,131 @@ Object { } `; +exports[`KQL Custom Transform Generator returns the expected transform params for timeslices slo using timesliceTarget = 0 1`] = ` +Object { + "_meta": Object { + "managed": true, + "managed_by": "observability", + "version": 3.2, + }, + "defer_validation": true, + "description": "Rolled-up SLI data for SLO: irrelevant [id: irrelevant, revision: 1]", + "dest": Object { + "index": ".slo-observability.sli-v3.2", + "pipeline": ".slo-observability.sli.pipeline-v3.2", + }, + "frequency": "1m", + "pivot": Object { + "aggregations": Object { + "slo.denominator": Object { + "filter": Object { + "match_all": Object {}, + }, + }, + "slo.isGoodSlice": Object { + "bucket_script": Object { + "buckets_path": Object { + "goodEvents": "slo.numerator>_count", + "totalEvents": "slo.denominator>_count", + }, + "script": "params.goodEvents / params.totalEvents > 0 ? 1 : 0", + }, + }, + "slo.numerator": Object { + "filter": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "range": Object { + "latency": Object { + "lt": "300", + }, + }, + }, + ], + }, + }, + }, + }, + "group_by": Object { + "@timestamp": Object { + "date_histogram": Object { + "field": "log_timestamp", + "fixed_interval": "2m", + }, + }, + "slo.id": Object { + "terms": Object { + "field": "slo.id", + }, + }, + "slo.revision": Object { + "terms": Object { + "field": "slo.revision", + }, + }, + }, + }, + "settings": Object { + "deduce_mappings": false, + "unattended": true, + }, + "source": Object { + "index": Array [ + "my-index*", + "my-other-index*", + ], + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "log_timestamp": Object { + "gte": "now-7d/d", + }, + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "labels.groupId": "group-3", + }, + }, + ], + }, + }, + ], + }, + }, + "runtime_mappings": Object { + "slo.id": Object { + "script": Object { + "source": "emit('irrelevant')", + }, + "type": "keyword", + }, + "slo.revision": Object { + "script": Object { + "source": "emit(1)", + }, + "type": "long", + }, + }, + }, + "sync": Object { + "time": Object { + "delay": "1m", + "field": "log_timestamp", + }, + }, + "transform_id": "slo-irrelevant-1", +} +`; + exports[`KQL Custom Transform Generator returns the expected transform params with every specified indicator params 1`] = ` Object { "_meta": Object { diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.test.ts index 6b5491e487191..4a0ade1c9aa68 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.test.ts @@ -6,6 +6,7 @@ */ import { ALL_VALUE } from '@kbn/slo-schema'; +import { twoMinute } from '../fixtures/duration'; import { createAPMTransactionDurationIndicator, createSLO, @@ -33,6 +34,21 @@ describe('APM Transaction Duration Transform Generator', () => { expect(transform).toMatchSnapshot(); }); + it('returns the expected transform params for timeslices slo using a timesliceTarget = 0', () => { + const slo = createSLOWithTimeslicesBudgetingMethod({ + id: 'irrelevant', + indicator: createAPMTransactionDurationIndicator(), + objective: { + target: 0.98, + timesliceTarget: 0, + timesliceWindow: twoMinute(), + }, + }); + const transform = generator.getTransformParams(slo); + + expect(transform).toMatchSnapshot(); + }); + it("does not include the query filter when params are '*'", () => { const slo = createSLO({ indicator: createAPMTransactionDurationIndicator({ diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts index 385025017044d..7dde0e0a664e3 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_duration.ts @@ -23,6 +23,7 @@ import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_tr import { APMTransactionDurationIndicator, SLODefinition } from '../../domain/models'; import { InvalidTransformError } from '../../errors'; import { parseIndex } from './common'; +import { getTimesliceTargetComparator } from './common'; export class ApmTransactionDurationTransformGenerator extends TransformGenerator { public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { @@ -179,7 +180,9 @@ export class ApmTransactionDurationTransformGenerator extends TransformGenerator goodEvents: 'slo.numerator.value', totalEvents: 'slo.denominator.value', }, - script: `params.goodEvents / params.totalEvents >= ${slo.objective.timesliceTarget} ? 1 : 0`, + script: `params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( + slo.objective.timesliceTarget! + )} ${slo.objective.timesliceTarget} ? 1 : 0`, }, }, }), diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.test.ts index 9934f5ea27a51..c9a85ae5df34e 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.test.ts @@ -6,6 +6,7 @@ */ import { ALL_VALUE } from '@kbn/slo-schema'; +import { twoMinute } from '../fixtures/duration'; import { createAPMTransactionErrorRateIndicator, createSLO, @@ -36,6 +37,21 @@ describe('APM Transaction Error Rate Transform Generator', () => { expect(transform).toMatchSnapshot(); }); + it('returns the expected transform params for timeslices slo using timesliceTarget = 0', async () => { + const slo = createSLOWithTimeslicesBudgetingMethod({ + id: 'irrelevant', + indicator: createAPMTransactionErrorRateIndicator(), + objective: { + target: 0.98, + timesliceTarget: 0, + timesliceWindow: twoMinute(), + }, + }); + const transform = generator.getTransformParams(slo); + + expect(transform).toMatchSnapshot(); + }); + it("does not include the query filter when params are '*'", async () => { const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator({ diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts index 8d91a1226d61c..99730152ea322 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/apm_transaction_error_rate.ts @@ -22,6 +22,7 @@ import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_tr import { APMTransactionErrorRateIndicator, SLODefinition } from '../../domain/models'; import { InvalidTransformError } from '../../errors'; import { parseIndex } from './common'; +import { getTimesliceTargetComparator } from './common'; export class ApmTransactionErrorRateTransformGenerator extends TransformGenerator { public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { @@ -162,7 +163,9 @@ export class ApmTransactionErrorRateTransformGenerator extends TransformGenerato goodEvents: 'slo.numerator>_count', totalEvents: 'slo.denominator>_count', }, - script: `params.goodEvents / params.totalEvents >= ${slo.objective.timesliceTarget} ? 1 : 0`, + script: `params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( + slo.objective.timesliceTarget! + )} ${slo.objective.timesliceTarget} ? 1 : 0`, }, }, }), diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/common.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/common.test.ts index fa34496573338..baf02a9102f02 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/common.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/common.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { parseIndex } from './common'; +import { getTimesliceTargetComparator, parseIndex } from './common'; describe('common', () => { describe('parseIndex', () => { @@ -20,4 +20,14 @@ describe('common', () => { expect(parseIndex(index)).toEqual(expected); }); }); + + describe('timeslice target comparator', () => { + it('returns GT when timeslice target is 0', () => { + expect(getTimesliceTargetComparator(0)).toBe('>'); + }); + + it('returns GTE when timeslice tyarnarget is not 0', () => { + expect(getTimesliceTargetComparator(0.000000001)).toBe('>='); + }); + }); }); diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/common.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/common.ts index e173aa24ce818..12822d6b41e68 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/common.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/common.ts @@ -35,3 +35,7 @@ export function parseIndex(index: string): string | string[] { return index.split(','); } + +export function getTimesliceTargetComparator(timesliceTarget: number) { + return timesliceTarget === 0 ? '>' : '>='; +} diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.test.ts index 77ef70e75ca5f..172d7c50cd59e 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { twoMinute } from '../fixtures/duration'; import { createHistogramIndicator, createSLO, @@ -69,6 +70,21 @@ describe('Histogram Transform Generator', () => { expect(transform).toMatchSnapshot(); }); + it('returns the expected transform params for timeslices slo using timesliceTarget = 0', async () => { + const anSLO = createSLOWithTimeslicesBudgetingMethod({ + id: 'irrelevant', + indicator: createHistogramIndicator(), + objective: { + target: 0.98, + timesliceTarget: 0, + timesliceWindow: twoMinute(), + }, + }); + const transform = generator.getTransformParams(anSLO); + + expect(transform).toMatchSnapshot(); + }); + it('filters the source using the kql query', async () => { const anSLO = createSLO({ indicator: createHistogramIndicator({ filter: 'labels.groupId: group-4' }), diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts index c6bce6a0b3c19..57fbaa630d367 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/histogram.ts @@ -21,6 +21,7 @@ import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_tr import { SLODefinition } from '../../domain/models'; import { InvalidTransformError } from '../../errors'; import { GetHistogramIndicatorAggregation } from '../aggregations'; +import { getTimesliceTargetComparator } from './common'; export class HistogramTransformGenerator extends TransformGenerator { public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { @@ -91,7 +92,9 @@ export class HistogramTransformGenerator extends TransformGenerator { goodEvents: 'slo.numerator>value', totalEvents: 'slo.denominator>value', }, - script: `params.goodEvents / params.totalEvents >= ${slo.objective.timesliceTarget} ? 1 : 0`, + script: `params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( + slo.objective.timesliceTarget! + )} ${slo.objective.timesliceTarget} ? 1 : 0`, }, }, }), diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.test.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.test.ts index 1512bb5a655ab..7489ebd06c5f4 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { twoMinute } from '../fixtures/duration'; import { createKQLCustomIndicator, createSLO, @@ -53,6 +54,21 @@ describe('KQL Custom Transform Generator', () => { expect(transform).toMatchSnapshot(); }); + it('returns the expected transform params for timeslices slo using timesliceTarget = 0', async () => { + const anSLO = createSLOWithTimeslicesBudgetingMethod({ + id: 'irrelevant', + indicator: createKQLCustomIndicator(), + objective: { + target: 0.98, + timesliceTarget: 0, + timesliceWindow: twoMinute(), + }, + }); + const transform = generator.getTransformParams(anSLO); + + expect(transform).toMatchSnapshot(); + }); + it('filters the source using the kql query', async () => { const anSLO = createSLO({ indicator: createKQLCustomIndicator({ filter: 'labels.groupId: group-4' }), diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts index 821e6c3197925..fb52772b762cd 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/kql_custom.ts @@ -7,16 +7,16 @@ import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; import { kqlCustomIndicatorSchema, timeslicesBudgetingMethodSchema } from '@kbn/slo-schema'; - -import { InvalidTransformError } from '../../errors'; -import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { getElasticsearchQueryOrThrow, parseIndex, TransformGenerator } from '.'; import { + getSLOTransformId, SLO_DESTINATION_INDEX_NAME, SLO_INGEST_PIPELINE_NAME, - getSLOTransformId, } from '../../../common/constants'; +import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_transform_template'; import { KQLCustomIndicator, SLODefinition } from '../../domain/models'; +import { InvalidTransformError } from '../../errors'; +import { getTimesliceTargetComparator } from './common'; export class KQLCustomTransformGenerator extends TransformGenerator { public getTransformParams(slo: SLODefinition): TransformPutTransformRequest { @@ -86,7 +86,9 @@ export class KQLCustomTransformGenerator extends TransformGenerator { goodEvents: 'slo.numerator>_count', totalEvents: 'slo.denominator>_count', }, - script: `params.goodEvents / params.totalEvents >= ${slo.objective.timesliceTarget} ? 1 : 0`, + script: `params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( + slo.objective.timesliceTarget! + )} ${slo.objective.timesliceTarget} ? 1 : 0`, }, }, }), diff --git a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts index 25dd36bd0b5d9..9242e80b09270 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/transform_generators/metric_custom.ts @@ -17,6 +17,7 @@ import { getSLOTransformTemplate } from '../../assets/transform_templates/slo_tr import { MetricCustomIndicator, SLODefinition } from '../../domain/models'; import { InvalidTransformError } from '../../errors'; import { GetCustomMetricIndicatorAggregation } from '../aggregations'; +import { getTimesliceTargetComparator } from './common'; export const INVALID_EQUATION_REGEX = /[^A-Z|+|\-|\s|\d+|\.|\(|\)|\/|\*|>|<|=|\?|\:|&|\!|\|]+/g; @@ -96,7 +97,9 @@ export class MetricCustomTransformGenerator extends TransformGenerator { goodEvents: 'slo.numerator>value', totalEvents: 'slo.denominator>value', }, - script: `params.goodEvents / params.totalEvents >= ${slo.objective.timesliceTarget} ? 1 : 0`, + script: `params.goodEvents / params.totalEvents ${getTimesliceTargetComparator( + slo.objective.timesliceTarget! + )} ${slo.objective.timesliceTarget} ? 1 : 0`, }, }, }), diff --git a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/common.ts b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/common.ts index ccfe213fb7d7c..585402f6f9774 100644 --- a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/common.ts +++ b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/common.ts @@ -9,10 +9,13 @@ import { isRight } from 'fp-ts/lib/Either'; import Mustache from 'mustache'; import { AlertsLocatorParams, getAlertUrl } from '@kbn/observability-plugin/common'; import { LocatorPublic } from '@kbn/share-plugin/common'; -import { legacyExperimentalFieldMap } from '@kbn/alerts-as-data-utils'; +import { legacyExperimentalFieldMap, ObservabilityUptimeAlert } from '@kbn/alerts-as-data-utils'; import { IBasePath } from '@kbn/core/server'; -import { type IRuleTypeAlerts, RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import type { IRuleTypeAlerts } from '@kbn/alerting-plugin/server'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; import { addSpaceIdToPath } from '@kbn/spaces-plugin/common'; +import { AlertInstanceState } from '@kbn/alerting-plugin/server'; +import { AlertInstanceContext } from '@kbn/alerting-plugin/server'; import { uptimeRuleFieldMap } from '../../../../common/rules/uptime_rule_field_map'; import { SYNTHETICS_RULE_TYPES_ALERT_CONTEXT } from '../../../../common/constants/synthetics_alerts'; import { UptimeCommonState, UptimeCommonStateType } from '../../../../common/runtime_types'; @@ -82,31 +85,29 @@ export const getAlertDetailsUrl = ( alertUuid: string | null ) => addSpaceIdToPath(basePath.publicBaseUrl, spaceId, `/app/observability/alerts/${alertUuid}`); -export const setRecoveredAlertsContext = async ({ - alertFactory, +export const setRecoveredAlertsContext = async ({ + alertsClient, + alertsLocator, basePath, defaultStartedAt, - getAlertStartedDate, spaceId, - alertsLocator, - getAlertUuid, }: { - alertFactory: RuleExecutorServices['alertFactory']; - defaultStartedAt: string; - getAlertStartedDate: (alertInstanceId: string) => string | null; + alertsClient: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + ActionGroupIds, + ObservabilityUptimeAlert + >['alertsClient']; + alertsLocator?: LocatorPublic; basePath: IBasePath; + defaultStartedAt: string; spaceId: string; - alertsLocator?: LocatorPublic; - getAlertUuid?: (alertId: string) => string | null; }) => { - const { getRecoveredAlerts } = alertFactory.done(); - - for await (const alert of getRecoveredAlerts()) { - const recoveredAlertId = alert.getId(); - const alertUuid = getAlertUuid?.(recoveredAlertId) || null; - const indexedStartedAt = getAlertStartedDate(recoveredAlertId) ?? defaultStartedAt; - - const state = alert.getState(); + for (const recoveredAlert of alertsClient?.getRecoveredAlerts() ?? []) { + const recoveredAlertId = recoveredAlert.alert.getId(); + const alertUuid = recoveredAlert.alert.getUuid(); + const indexedStartedAt = recoveredAlert.alert.getStart() ?? defaultStartedAt; + const state = recoveredAlert.alert.getState(); const alertUrl = await getAlertUrl( alertUuid, spaceId, @@ -115,17 +116,21 @@ export const setRecoveredAlertsContext = async ({ basePath.publicBaseUrl ); - alert.setContext({ - ...state, - [ALERT_DETAILS_URL]: alertUrl, + alertsClient!.setAlertData({ + id: recoveredAlertId, + context: { + ...state, + [ALERT_DETAILS_URL]: alertUrl, + }, }); } }; export const uptimeRuleTypeFieldMap = { ...uptimeRuleFieldMap, ...legacyExperimentalFieldMap }; -export const UptimeRuleTypeAlertDefinition: IRuleTypeAlerts = { +export const UptimeRuleTypeAlertDefinition: IRuleTypeAlerts = { context: SYNTHETICS_RULE_TYPES_ALERT_CONTEXT, mappings: { fieldMap: uptimeRuleTypeFieldMap }, useLegacyAlerts: true, + shouldWrite: true, }; diff --git a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/duration_anomaly.test.ts b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/duration_anomaly.test.ts index 7a48860265432..2d586b3a336c0 100644 --- a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/duration_anomaly.test.ts +++ b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/duration_anomaly.test.ts @@ -11,11 +11,8 @@ import { } from '@kbn/rule-data-utils'; import { durationAnomalyAlertFactory } from './duration_anomaly'; import { DURATION_ANOMALY } from '../../../../common/constants/uptime_alerts'; -import { - getSeverityType, - type MlAnomaliesTableRecord, - type MlAnomalyRecordDoc, -} from '@kbn/ml-anomaly-utils'; +import { getSeverityType } from '@kbn/ml-anomaly-utils'; +import type { MlAnomaliesTableRecord, MlAnomalyRecordDoc } from '@kbn/ml-anomaly-utils'; import { createRuleTypeMocks, bootstrapDependencies } from './test_utils'; import { Ping } from '../../../../common/runtime_types/ping'; @@ -104,6 +101,27 @@ const mockOptions = ( ): any => { const { services, setContext } = createRuleTypeMocks(mockRecoveredAlerts); + services.alertsClient.report.mockImplementation((param: any) => { + return { + uuid: `uuid-${param.id}`, + start: new Date().toISOString(), + alertDoc: {}, + }; + }); + + services.alertsClient.getRecoveredAlerts.mockImplementation((param: any) => { + return mockRecoveredAlerts.map((alert) => ({ + alert: { + getId: () => 'mock-id', + getUuid: () => 'mock-uuiid', + getState: () => alert, + getStart: () => new Date().toISOString(), + setContext, + context: {}, + }, + })); + }); + return { params, state, @@ -158,12 +176,12 @@ describe('duration anomaly alert', () => { const alert = durationAnomalyAlertFactory(server, libs, plugins); const options = mockOptions(); const { - services: { alertWithLifecycle }, + services: { alertsClient }, } = options; // @ts-ignore the executor can return `void`, but ours never does const state: Record = await alert.executor(options); expect(mockGetAnomliesTableDataGetter).toHaveBeenCalledTimes(1); - expect(alertWithLifecycle).toHaveBeenCalledTimes(2); + expect(alertsClient.report).toHaveBeenCalledTimes(2); expect(mockGetAnomliesTableDataGetter).toBeCalledWith( ['uptime_monitor_high_latency_by_geo'], [], @@ -177,14 +195,15 @@ describe('duration anomaly alert', () => { 10, undefined ); - const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; - expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2); + const reasonMessages: string[] = []; mockAnomaliesResult.anomalies.forEach((anomaly, index) => { const slowestResponse = Math.round(anomaly.actualSort / 1000); const typicalResponse = Math.round(anomaly.typicalSort / 1000); - expect(alertWithLifecycle).toBeCalledWith({ - fields: { + expect(alertsClient.report).toHaveBeenCalledWith({ + id: `${DURATION_ANOMALY.id}${index}`, + actionGroup: DURATION_ANOMALY.id, + payload: { 'monitor.id': options.params.monitorId, 'url.full': mockPing.url?.full, 'anomaly.start': mockDate, @@ -201,27 +220,26 @@ Response times as high as ${slowestResponse} ms have been detected from location anomaly.entityValue }. Expected response time is ${typicalResponse} ms.`, }, - id: `${DURATION_ANOMALY.id}${index}`, + state: { + firstCheckedAt: 'date', + firstTriggeredAt: undefined, + lastCheckedAt: 'date', + lastResolvedAt: undefined, + isTriggered: false, + anomalyStartTimestamp: 'date', + currentTriggerStarted: undefined, + expectedResponseTime: `${typicalResponse} ms`, + lastTriggeredAt: undefined, + monitor: monitorId, + monitorUrl: mockPing.url?.full, + observerLocation: anomaly.entityValue, + severity: getSeverityType(anomaly.severity), + severityScore: anomaly.severity, + slowestAnomalyResponse: `${slowestResponse} ms`, + bucketSpan: anomaly.source.bucket_span, + }, }); - expect(alertInstanceMock.replaceState).toBeCalledWith({ - firstCheckedAt: 'date', - firstTriggeredAt: undefined, - lastCheckedAt: 'date', - lastResolvedAt: undefined, - isTriggered: false, - anomalyStartTimestamp: 'date', - currentTriggerStarted: undefined, - expectedResponseTime: `${typicalResponse} ms`, - lastTriggeredAt: undefined, - monitor: monitorId, - monitorUrl: mockPing.url?.full, - observerLocation: anomaly.entityValue, - severity: getSeverityType(anomaly.severity), - severityScore: anomaly.severity, - slowestAnomalyResponse: `${slowestResponse} ms`, - bucketSpan: anomaly.source.bucket_span, - }); const reasonMsg = `Abnormal (${getSeverityType( anomaly.severity )} level) response time detected on uptime-monitor with url ${ @@ -233,45 +251,48 @@ Response times as high as ${slowestResponse} ms have been detected from location reasonMessages.push(reasonMsg); }); - expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(2); - expect(alertInstanceMock.scheduleActions.mock.calls[0]).toMatchInlineSnapshot(` + expect(alertsClient.setAlertData.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "xpack.uptime.alerts.actionGroups.durationAnomaly", Object { - "alertDetailsUrl": "mockedAlertsLocator > getLocation", - "anomalyStartTimestamp": "date", - "bucketSpan": 900, - "expectedResponseTime": "10 ms", - "monitor": "uptime-monitor", - "monitorUrl": "https://elastic.co", - "observerLocation": "harrisburg", - "reason": "Abnormal (minor level) response time detected on uptime-monitor with url https://elastic.co at date. Anomaly severity score is 25. + "context": Object { + "alertDetailsUrl": "mockedAlertsLocator > getLocation", + "anomalyStartTimestamp": "date", + "bucketSpan": 900, + "expectedResponseTime": "10 ms", + "monitor": "uptime-monitor", + "monitorUrl": "https://elastic.co", + "observerLocation": "harrisburg", + "reason": "Abnormal (minor level) response time detected on uptime-monitor with url https://elastic.co at date. Anomaly severity score is 25. Response times as high as 200 ms have been detected from location harrisburg. Expected response time is 10 ms.", - "severity": "minor", - "severityScore": 25, - "slowestAnomalyResponse": "200 ms", - "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/eHBhY2sudXB0aW1lLmFsZXJ0cy5hY3Rpb25Hcm91cHMuZHVyYXRpb25Bbm9tYWx5MA==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z", + "severity": "minor", + "severityScore": 25, + "slowestAnomalyResponse": "200 ms", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/eHBhY2sudXB0aW1lLmFsZXJ0cy5hY3Rpb25Hcm91cHMuZHVyYXRpb25Bbm9tYWx5MA==?dateRangeEnd=now&dateRangeStart=date", + }, + "id": "xpack.uptime.alerts.actionGroups.durationAnomaly", }, ] `); - expect(alertInstanceMock.scheduleActions.mock.calls[1]).toMatchInlineSnapshot(` + expect(alertsClient.setAlertData.mock.calls[1]).toMatchInlineSnapshot(` Array [ - "xpack.uptime.alerts.actionGroups.durationAnomaly", Object { - "alertDetailsUrl": "mockedAlertsLocator > getLocation", - "anomalyStartTimestamp": "date", - "bucketSpan": 900, - "expectedResponseTime": "20 ms", - "monitor": "uptime-monitor", - "monitorUrl": "https://elastic.co", - "observerLocation": "fairbanks", - "reason": "Abnormal (warning level) response time detected on uptime-monitor with url https://elastic.co at date. Anomaly severity score is 10. + "context": Object { + "alertDetailsUrl": "mockedAlertsLocator > getLocation", + "anomalyStartTimestamp": "date", + "bucketSpan": 900, + "expectedResponseTime": "20 ms", + "monitor": "uptime-monitor", + "monitorUrl": "https://elastic.co", + "observerLocation": "fairbanks", + "reason": "Abnormal (warning level) response time detected on uptime-monitor with url https://elastic.co at date. Anomaly severity score is 10. Response times as high as 300 ms have been detected from location fairbanks. Expected response time is 20 ms.", - "severity": "warning", - "severityScore": 10, - "slowestAnomalyResponse": "300 ms", - "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/eHBhY2sudXB0aW1lLmFsZXJ0cy5hY3Rpb25Hcm91cHMuZHVyYXRpb25Bbm9tYWx5MQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z", + "severity": "warning", + "severityScore": 10, + "slowestAnomalyResponse": "300 ms", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/eHBhY2sudXB0aW1lLmFsZXJ0cy5hY3Rpb25Hcm91cHMuZHVyYXRpb25Bbm9tYWx5MQ==?dateRangeEnd=now&dateRangeStart=date", + }, + "id": "xpack.uptime.alerts.actionGroups.durationAnomaly", }, ] `); @@ -300,11 +321,17 @@ Response times as high as ${slowestResponse} ms have been detected from location ); const alert = durationAnomalyAlertFactory(server, libs, plugins); const options = mockOptions(); + const { + services: { alertsClient }, + } = options; // @ts-ignore the executor can return `void`, but ours never does const state: Record = await alert.executor(options); - expect(options.setContext).toHaveBeenCalledTimes(2); - mockRecoveredAlerts.forEach((alertState) => { - expect(options.setContext).toHaveBeenCalledWith(alertState); + expect(alertsClient.setAlertData).toHaveBeenCalledTimes(4); + mockRecoveredAlerts.forEach((alertState, index) => { + expect(alertsClient.setAlertData).toHaveBeenNthCalledWith(index + 3, { + context: alertState, + id: 'mock-id', + }); }); }); }); diff --git a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/duration_anomaly.ts b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/duration_anomaly.ts index 7de5a3b1b5327..4bb64fe694446 100644 --- a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/duration_anomaly.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; +import { AlertsClientError, GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; import moment from 'moment'; import { KibanaRequest, @@ -19,7 +19,8 @@ import { ALERT_REASON, } from '@kbn/rule-data-utils'; import { ActionGroupIdsOf } from '@kbn/alerting-plugin/common'; -import { getSeverityType, type MlAnomaliesTableRecord } from '@kbn/ml-anomaly-utils'; +import type { MlAnomaliesTableRecord } from '@kbn/ml-anomaly-utils'; +import { getSeverityType } from '@kbn/ml-anomaly-utils'; import { alertsLocatorID, AlertsLocatorParams, @@ -136,18 +137,14 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory doesSetRecoveryContext: true, async executor({ params, - services: { - alertFactory, - alertWithLifecycle, - getAlertStartedDate, - getAlertUuid, - savedObjectsClient, - scopedClusterClient, - }, + services: { alertsClient, savedObjectsClient, scopedClusterClient }, spaceId, state, startedAt, }) { + if (!alertsClient) { + throw new AlertsClientError(); + } const uptimeEsClient = new UptimeEsClient( savedObjectsClient, scopedClusterClient.asCurrentUser, @@ -181,53 +178,56 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory ); const alertId = DURATION_ANOMALY.id + index; - const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); - const alertUuid = getAlertUuid(alertId); - const relativeViewInAppUrl = getMonitorRouteFromMonitorId({ - monitorId: alertId, - dateRangeEnd: 'now', - dateRangeStart: indexedStartedAt, - }); - const alert = alertWithLifecycle({ + const { start, uuid } = alertsClient?.report({ id: alertId, - fields: { + actionGroup: DURATION_ANOMALY.id, + payload: { 'monitor.id': params.monitorId, 'url.full': summary.monitorUrl, 'observer.geo.name': summary.observerLocation, 'anomaly.start': summary.anomalyStartTimestamp, - 'anomaly.bucket_span.minutes': summary.bucketSpan, + 'anomaly.bucket_span.minutes': summary.bucketSpan as unknown as string, [ALERT_EVALUATION_VALUE]: anomaly.actualSort, [ALERT_EVALUATION_THRESHOLD]: anomaly.typicalSort, [ALERT_REASON]: alertReasonMessage, }, + state: { + ...updateState(state, false), + ...summary, + }, }); - alert.replaceState({ - ...updateState(state, false), - ...summary, + + const indexedStartedAt = start ?? startedAt.toISOString(); + const relativeViewInAppUrl = getMonitorRouteFromMonitorId({ + monitorId: alertId, + dateRangeEnd: 'now', + dateRangeStart: indexedStartedAt, }); - alert.scheduleActions(DURATION_ANOMALY.id, { - [ALERT_DETAILS_URL]: await getAlertUrl( - alertUuid, - spaceId, - indexedStartedAt, - alertsLocator, - basePath.publicBaseUrl - ), - [ALERT_REASON_MSG]: alertReasonMessage, - [VIEW_IN_APP_URL]: getViewInAppUrl(basePath, spaceId, relativeViewInAppUrl), - ...summary, + + alertsClient.setAlertData({ + id: DURATION_ANOMALY.id, + context: { + [ALERT_DETAILS_URL]: await getAlertUrl( + uuid, + spaceId, + indexedStartedAt, + alertsLocator, + basePath.publicBaseUrl + ), + [ALERT_REASON_MSG]: alertReasonMessage, + [VIEW_IN_APP_URL]: getViewInAppUrl(basePath, spaceId, relativeViewInAppUrl), + ...summary, + }, }); }); } - await setRecoveredAlertsContext({ - alertFactory, + await setRecoveredAlertsContext({ + alertsClient, alertsLocator, basePath, defaultStartedAt: startedAt.toISOString(), - getAlertStartedDate, - getAlertUuid, spaceId, }); diff --git a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.test.ts b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.test.ts index 9777ecff6b14a..7d162b65b6484 100644 --- a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.test.ts +++ b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.test.ts @@ -121,20 +121,23 @@ const mockStatusAlertDocument = ( isAutoGenerated: boolean = false, count: number, interval: string, - numTimes: number + numTimes: number, + actionGroup: string ) => { const { monitorInfo } = monitor; const checkedAt = moment(monitorInfo.timestamp).format('LLL'); return { - fields: { + payload: { ...mockCommonAlertDocumentFields(monitor.monitorInfo), [ALERT_REASON]: `Monitor "First" from ${monitor.monitorInfo.observer?.geo?.name} failed ${count} times in the last ${interval}. Alert when >= ${numTimes}. Checked at ${checkedAt}.`, }, + actionGroup, id: getInstanceId( monitorInfo, `${isAutoGenerated ? '' : monitorInfo?.monitor.id + '-'}${monitorInfo.observer?.geo?.name}` ), + state: expect.any(Object), }; }; @@ -144,7 +147,8 @@ const mockAvailabilityAlertDocument = (monitor: GetMonitorAvailabilityResult) => const checkedAt = moment(monitorInfo.timestamp).format('LLL'); return { - fields: { + actionGroup: 'xpack.uptime.alerts.actionGroups.monitorStatus', + payload: { ...mockCommonAlertDocumentFields(monitor.monitorInfo), [ALERT_REASON]: `Monitor "${monitorInfo.monitor.name || monitorInfo.monitor.id}" from ${ monitorInfo.observer?.geo?.name @@ -152,6 +156,7 @@ const mockAvailabilityAlertDocument = (monitor: GetMonitorAvailabilityResult) => 2 )}%. Alert when < 99.34%. Checked at ${checkedAt}.`, }, + state: expect.any(Object), id: getInstanceId(monitorInfo, `${monitorInfo?.monitor.id}-${monitorInfo.observer?.geo?.name}`), }; }; @@ -174,10 +179,32 @@ const mockOptions = ( schedule: { interval: '5m', }, - } + }, + recoveredAlerts: typeof mockRecoveredAlerts = [] ): any => { const { services, setContext } = createRuleTypeMocks(mockRecoveredAlerts); + services.alertsClient.report.mockImplementation((param: any) => { + return { + uuid: `uuid-${param.id}`, + start: new Date().toISOString(), + alertDoc: {}, + }; + }); + + services.alertsClient.getRecoveredAlerts.mockImplementation((param: any) => { + return recoveredAlerts.map((alert) => ({ + alert: { + getId: () => 'mock-id', + getUuid: () => 'mock-uuid', + getState: () => alert, + getStart: () => new Date().toISOString(), + setContext, + context: {}, + }, + })); + }); + return { params, state, @@ -251,17 +278,76 @@ describe('status check alert', () => { timerangeCount: 15, }); const { - services: { alertWithLifecycle }, + services: { alertsClient }, } = options; // @ts-ignore the executor can return `void`, but ours never does const state: Record = await alert.executor(options); expect(mockGetter).toHaveBeenCalledTimes(1); - expect(alertWithLifecycle).toHaveBeenCalledTimes(2); + expect(alertsClient.report).toHaveBeenCalledTimes(2); mockMonitors.forEach((monitor) => { - expect(alertWithLifecycle).toBeCalledWith( - mockStatusAlertDocument(monitor, false, 234, '15 mins', 5) + expect(alertsClient.report).toBeCalledWith( + mockStatusAlertDocument( + monitor, + false, + 234, + '15 mins', + 5, + 'xpack.uptime.alerts.actionGroups.monitorStatus' + ) ); }); + + expect(alertsClient.report).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + state: { + checkedAt: 'July 6, 2020 9:14 PM', + currentTriggerStarted: 'foo date string', + firstCheckedAt: 'foo date string', + firstTriggeredAt: 'foo date string', + isTriggered: true, + lastCheckedAt: 'foo date string', + lastResolvedAt: undefined, + lastTriggeredAt: 'foo date string', + latestErrorMessage: 'error message 1', + monitorId: 'first', + monitorName: 'First', + monitorType: 'myType', + monitorUrl: 'localhost:8080', + observerHostname: undefined, + observerLocation: 'harrisburg', + reason: `Monitor "First" from harrisburg failed 234 times in the last 15 mins. Alert when >= 5. Checked at July 6, 2020 9:14 PM.`, + statusMessage: 'failed 234 times in the last 15 mins. Alert when >= 5.', + }, + }) + ); + + expect(alertsClient.report).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + state: { + checkedAt: 'July 6, 2020 9:14 PM', + currentTriggerStarted: 'foo date string', + firstCheckedAt: 'foo date string', + firstTriggeredAt: 'foo date string', + isTriggered: true, + lastCheckedAt: 'foo date string', + lastResolvedAt: undefined, + lastTriggeredAt: 'foo date string', + latestErrorMessage: 'error message 2', + monitorId: 'first', + monitorName: 'First', + monitorType: 'myType', + monitorUrl: 'localhost:5601', + observerHostname: undefined, + observerLocation: 'fairbanks', + reason: + 'Monitor "First" from fairbanks failed 234 times in the last 15 mins. Alert when >= 5. Checked at July 6, 2020 9:14 PM.', + statusMessage: 'failed 234 times in the last 15 mins. Alert when >= 5.', + }, + }) + ); + expect(mockGetter.mock.calls[0][0]).toEqual( expect.objectContaining({ filters: undefined, @@ -273,53 +359,25 @@ describe('status check alert', () => { }, }) ); - const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; - expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2); - expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "checkedAt": "July 6, 2020 9:14 PM", - "configId": undefined, - "currentTriggerStarted": "foo date string", - "firstCheckedAt": "foo date string", - "firstTriggeredAt": "foo date string", - "isTriggered": true, - "lastCheckedAt": "foo date string", - "lastResolvedAt": undefined, - "lastTriggeredAt": "foo date string", - "latestErrorMessage": "error message 1", - "monitorId": "first", - "monitorName": "First", - "monitorType": "myType", - "monitorUrl": "localhost:8080", - "observerHostname": undefined, - "observerLocation": "harrisburg", - "reason": "Monitor \\"First\\" from harrisburg failed 234 times in the last 15 mins. Alert when >= 5. Checked at July 6, 2020 9:14 PM.", - "statusMessage": "failed 234 times in the last 15 mins. Alert when >= 5.", - }, - ] - `); - expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(2); - expect(alertInstanceMock.scheduleActions.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "xpack.uptime.alerts.actionGroups.monitorStatus", - Object { - "alertDetailsUrl": "mockedAlertsLocator > getLocation", - "checkedAt": "July 6, 2020 9:14 PM", - "configId": undefined, - "latestErrorMessage": "error message 1", - "monitorId": "first", - "monitorName": "First", - "monitorType": "myType", - "monitorUrl": "localhost:8080", - "observerHostname": undefined, - "observerLocation": "harrisburg", - "reason": "Monitor \\"First\\" from harrisburg failed 234 times in the last 15 mins. Alert when >= 5. Checked at July 6, 2020 9:14 PM.", - "statusMessage": "failed 234 times in the last 15 mins. Alert when >= 5.", - "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zmlyc3Q=?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", - }, - ] - `); + + expect(alertsClient.setAlertData).toHaveBeenNthCalledWith(1, { + id: 'first_localhost_8080_first-harrisburg', + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + checkedAt: 'July 6, 2020 9:14 PM', + latestErrorMessage: 'error message 1', + monitorId: 'first', + monitorName: 'First', + monitorType: 'myType', + monitorUrl: 'localhost:8080', + observerHostname: undefined, + observerLocation: 'harrisburg', + reason: `Monitor "First" from harrisburg failed 234 times in the last 15 mins. Alert when >= 5. Checked at July 6, 2020 9:14 PM.`, + statusMessage: 'failed 234 times in the last 15 mins. Alert when >= 5.', + viewInAppUrl: + 'http://localhost:5601/hfe/app/uptime/monitor/Zmlyc3Q=?dateRangeEnd=now&dateRangeStart=foo%20date%20string&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D', + }, + }); }); it('supports auto generated monitor status alerts', async () => { @@ -335,15 +393,22 @@ describe('status check alert', () => { numTimes: 5, }); const { - services: { alertWithLifecycle }, + services: { alertsClient }, } = options; // @ts-ignore the executor can return `void`, but ours never does const state: Record = await alert.executor(options); expect(mockGetter).toHaveBeenCalledTimes(1); - expect(alertWithLifecycle).toHaveBeenCalledTimes(2); + expect(alertsClient.report).toHaveBeenCalledTimes(2); mockMonitors.forEach((monitor) => { - expect(alertWithLifecycle).toBeCalledWith( - mockStatusAlertDocument(monitor, true, 234, '15m', 5) + expect(alertsClient.report).toBeCalledWith( + mockStatusAlertDocument( + monitor, + true, + 234, + '15m', + 5, + 'xpack.uptime.alerts.actionGroups.monitorStatus' + ) ); }); expect(mockGetter.mock.calls[0][0]).toEqual( @@ -357,53 +422,50 @@ describe('status check alert', () => { }, }) ); - const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; - expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2); - expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "checkedAt": "July 6, 2020 9:14 PM", - "configId": undefined, - "currentTriggerStarted": "foo date string", - "firstCheckedAt": "foo date string", - "firstTriggeredAt": "foo date string", - "isTriggered": true, - "lastCheckedAt": "foo date string", - "lastResolvedAt": undefined, - "lastTriggeredAt": "foo date string", - "latestErrorMessage": "error message 1", - "monitorId": "first", - "monitorName": "First", - "monitorType": "myType", - "monitorUrl": "localhost:8080", - "observerHostname": undefined, - "observerLocation": "harrisburg", - "reason": "Monitor \\"First\\" from harrisburg failed 234 times in the last 15m. Alert when >= 5. Checked at July 6, 2020 9:14 PM.", - "statusMessage": "failed 234 times in the last 15m. Alert when >= 5.", - }, - ] - `); - expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(2); - expect(alertInstanceMock.scheduleActions.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "xpack.uptime.alerts.actionGroups.monitorStatus", - Object { - "alertDetailsUrl": "mockedAlertsLocator > getLocation", - "checkedAt": "July 6, 2020 9:14 PM", - "configId": undefined, - "latestErrorMessage": "error message 1", - "monitorId": "first", - "monitorName": "First", - "monitorType": "myType", - "monitorUrl": "localhost:8080", - "observerHostname": undefined, - "observerLocation": "harrisburg", - "reason": "Monitor \\"First\\" from harrisburg failed 234 times in the last 15m. Alert when >= 5. Checked at July 6, 2020 9:14 PM.", - "statusMessage": "failed 234 times in the last 15m. Alert when >= 5.", - "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zmlyc3Q=?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", + + expect(alertsClient.report).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + state: { + checkedAt: 'July 6, 2020 9:14 PM', + currentTriggerStarted: 'foo date string', + firstCheckedAt: 'foo date string', + firstTriggeredAt: 'foo date string', + isTriggered: true, + lastCheckedAt: 'foo date string', + lastResolvedAt: undefined, + lastTriggeredAt: 'foo date string', + latestErrorMessage: 'error message 1', + monitorId: 'first', + monitorName: 'First', + monitorType: 'myType', + monitorUrl: 'localhost:8080', + observerHostname: undefined, + observerLocation: 'harrisburg', + reason: `Monitor "First" from harrisburg failed 234 times in the last 15m. Alert when >= 5. Checked at July 6, 2020 9:14 PM.`, + statusMessage: 'failed 234 times in the last 15m. Alert when >= 5.', }, - ] - `); + }) + ); + + expect(alertsClient.setAlertData).toHaveBeenNthCalledWith(1, { + id: 'first_localhost_8080_harrisburg', + context: { + alertDetailsUrl: 'mockedAlertsLocator > getLocation', + checkedAt: 'July 6, 2020 9:14 PM', + latestErrorMessage: 'error message 1', + monitorId: 'first', + monitorName: 'First', + monitorType: 'myType', + monitorUrl: 'localhost:8080', + observerHostname: undefined, + observerLocation: 'harrisburg', + reason: `Monitor "First" from harrisburg failed 234 times in the last 15m. Alert when >= 5. Checked at July 6, 2020 9:14 PM.`, + statusMessage: 'failed 234 times in the last 15m. Alert when >= 5.', + viewInAppUrl: + 'http://localhost:5601/hfe/app/uptime/monitor/Zmlyc3Q=?dateRangeEnd=now&dateRangeStart=foo%20date%20string&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D', + }, + }); }); it('supports 7.7 alert format', async () => { @@ -423,38 +485,68 @@ describe('status check alert', () => { filters: '', }); const { - services: { alertWithLifecycle }, + services: { alertsClient }, } = options; const executorResult = await alert.executor(options); - const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; + expect(alertsClient.report).toHaveBeenCalledTimes(2); + + expect(alertsClient.report).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + state: { + checkedAt: 'July 6, 2020 9:14 PM', + currentTriggerStarted: '7.7 date', + firstCheckedAt: '7.7 date', + firstTriggeredAt: '7.7 date', + isTriggered: true, + lastCheckedAt: '7.7 date', + lastTriggeredAt: '7.7 date', + latestErrorMessage: 'error message 1', + monitorId: 'first', + monitorName: 'First', + monitorType: 'myType', + monitorUrl: 'localhost:8080', + observerLocation: 'harrisburg', + reason: + 'Monitor "First" from harrisburg failed 234 times in the last 14h. Alert when >= 4. Checked at July 6, 2020 9:14 PM.', + statusMessage: 'failed 234 times in the last 14h. Alert when >= 4.', + }, + }) + ); + mockMonitors.forEach((monitor) => { - expect(alertWithLifecycle).toBeCalledWith( - mockStatusAlertDocument(monitor, false, 234, '14h', 4) + expect(alertsClient.report).toBeCalledWith( + mockStatusAlertDocument( + monitor, + false, + 234, + '14h', + 4, + 'xpack.uptime.alerts.actionGroups.monitorStatus' + ) ); }); - expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2); - expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` + expect(alertsClient.setAlertData).toHaveBeenCalledTimes(2); + expect(alertsClient.setAlertData.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "checkedAt": "July 6, 2020 9:14 PM", - "configId": undefined, - "currentTriggerStarted": "7.7 date", - "firstCheckedAt": "7.7 date", - "firstTriggeredAt": "7.7 date", - "isTriggered": true, - "lastCheckedAt": "7.7 date", - "lastResolvedAt": undefined, - "lastTriggeredAt": "7.7 date", - "latestErrorMessage": "error message 1", - "monitorId": "first", - "monitorName": "First", - "monitorType": "myType", - "monitorUrl": "localhost:8080", - "observerHostname": undefined, - "observerLocation": "harrisburg", - "reason": "Monitor \\"First\\" from harrisburg failed 234 times in the last 14h. Alert when >= 4. Checked at July 6, 2020 9:14 PM.", - "statusMessage": "failed 234 times in the last 14h. Alert when >= 4.", + "context": Object { + "alertDetailsUrl": "mockedAlertsLocator > getLocation", + "checkedAt": "July 6, 2020 9:14 PM", + "configId": undefined, + "latestErrorMessage": "error message 1", + "monitorId": "first", + "monitorName": "First", + "monitorType": "myType", + "monitorUrl": "localhost:8080", + "observerHostname": undefined, + "observerLocation": "harrisburg", + "reason": "Monitor \\"First\\" from harrisburg failed 234 times in the last 14h. Alert when >= 4. Checked at July 6, 2020 9:14 PM.", + "statusMessage": "failed 234 times in the last 14h. Alert when >= 4.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zmlyc3Q=?dateRangeEnd=now&dateRangeStart=7.7%20date&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", + }, + "id": "first_localhost_8080_first-harrisburg", }, ] `); @@ -496,12 +588,19 @@ describe('status check alert', () => { }); const executorResult = await alert.executor(options); const { - services: { alertWithLifecycle }, + services: { alertsClient }, } = options; - const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; + mockMonitors.forEach((monitor) => { - expect(alertWithLifecycle).toBeCalledWith( - mockStatusAlertDocument(monitor, false, 234, '15 mins', 3) + expect(alertsClient.report).toBeCalledWith( + mockStatusAlertDocument( + monitor, + false, + 234, + '15 mins', + 3, + 'xpack.uptime.alerts.actionGroups.monitorStatus' + ) ); }); expect(mockGetter).toHaveBeenCalledTimes(1); @@ -651,28 +750,26 @@ describe('status check alert', () => { }, } `); - expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2); - expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` + expect(alertsClient.setAlertData).toHaveBeenCalledTimes(2); + expect(alertsClient.setAlertData.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "checkedAt": "July 6, 2020 9:14 PM", - "configId": undefined, - "currentTriggerStarted": "foo date string", - "firstCheckedAt": "foo date string", - "firstTriggeredAt": "foo date string", - "isTriggered": true, - "lastCheckedAt": "foo date string", - "lastResolvedAt": undefined, - "lastTriggeredAt": "foo date string", - "latestErrorMessage": "error message 1", - "monitorId": "first", - "monitorName": "First", - "monitorType": "myType", - "monitorUrl": "localhost:8080", - "observerHostname": undefined, - "observerLocation": "harrisburg", - "reason": "Monitor \\"First\\" from harrisburg failed 234 times in the last 15 mins. Alert when >= 3. Checked at July 6, 2020 9:14 PM.", - "statusMessage": "failed 234 times in the last 15 mins. Alert when >= 3.", + "context": Object { + "alertDetailsUrl": "mockedAlertsLocator > getLocation", + "checkedAt": "July 6, 2020 9:14 PM", + "configId": undefined, + "latestErrorMessage": "error message 1", + "monitorId": "first", + "monitorName": "First", + "monitorType": "myType", + "monitorUrl": "localhost:8080", + "observerHostname": undefined, + "observerLocation": "harrisburg", + "reason": "Monitor \\"First\\" from harrisburg failed 234 times in the last 15 mins. Alert when >= 3. Checked at July 6, 2020 9:14 PM.", + "statusMessage": "failed 234 times in the last 15 mins. Alert when >= 3.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zmlyc3Q=?dateRangeEnd=now&dateRangeStart=foo%20date%20string&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", + }, + "id": "first_localhost_8080_first-harrisburg", }, ] `); @@ -758,7 +855,7 @@ describe('status check alert', () => { }); it('supports availability checks', async () => { - expect.assertions(13); + // expect.assertions(13); toISOStringSpy.mockImplementation(() => 'availability test'); const mockGetter: jest.Mock = jest.fn(); mockGetter.mockReturnValue([]); @@ -839,114 +936,123 @@ describe('status check alert', () => { shouldCheckStatus: false, }); const { - services: { alertWithLifecycle }, + services: { alertsClient }, } = options; const executorResult = await alert.executor(options); - const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; mockAvailabilityMonitors.forEach((monitor) => { - expect(alertWithLifecycle).toBeCalledWith(mockAvailabilityAlertDocument(monitor)); + expect(alertsClient.report).toBeCalledWith(mockAvailabilityAlertDocument(monitor)); }); - expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(4); - expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "checkedAt": "July 6, 2020 9:14 PM", - "configId": undefined, - "currentTriggerStarted": "availability test", - "firstCheckedAt": "availability test", - "firstTriggeredAt": "availability test", - "isTriggered": true, - "lastCheckedAt": "availability test", - "lastResolvedAt": undefined, - "lastTriggeredAt": "availability test", - "latestErrorMessage": undefined, - "monitorId": "foo", - "monitorName": "Foo", - "monitorType": "myType", - "monitorUrl": "https://foo.com", - "observerHostname": undefined, - "observerLocation": "harrisburg", - "reason": "Monitor \\"Foo\\" from harrisburg 35 days availability is 99.28%. Alert when < 99.34%. Checked at July 6, 2020 9:14 PM.", - "statusMessage": "35 days availability is 99.28%. Alert when < 99.34%.", + expect(alertsClient.report).toHaveBeenCalledTimes(4); + expect(alertsClient.report).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + state: { + checkedAt: 'July 6, 2020 9:14 PM', + currentTriggerStarted: 'availability test', + firstCheckedAt: 'availability test', + firstTriggeredAt: 'availability test', + isTriggered: true, + lastCheckedAt: 'availability test', + lastResolvedAt: undefined, + lastTriggeredAt: 'availability test', + latestErrorMessage: undefined, + monitorId: 'foo', + monitorName: 'Foo', + monitorType: 'myType', + monitorUrl: 'https://foo.com', + observerHostname: undefined, + observerLocation: 'harrisburg', + reason: + 'Monitor "Foo" from harrisburg 35 days availability is 99.28%. Alert when < 99.34%. Checked at July 6, 2020 9:14 PM.', + statusMessage: '35 days availability is 99.28%. Alert when < 99.34%.', }, - ] - `); - expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(4); - expect(alertInstanceMock.scheduleActions.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "xpack.uptime.alerts.actionGroups.monitorStatus", - Object { - "alertDetailsUrl": "mockedAlertsLocator > getLocation", - "checkedAt": "July 6, 2020 9:14 PM", - "configId": undefined, - "latestErrorMessage": undefined, - "monitorId": "foo", - "monitorName": "Foo", - "monitorType": "myType", - "monitorUrl": "https://foo.com", - "observerHostname": undefined, - "observerLocation": "harrisburg", - "reason": "Monitor \\"Foo\\" from harrisburg 35 days availability is 99.28%. Alert when < 99.34%. Checked at July 6, 2020 9:14 PM.", - "statusMessage": "35 days availability is 99.28%. Alert when < 99.34%.", - "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zm9v?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", - }, - ], - Array [ - "xpack.uptime.alerts.actionGroups.monitorStatus", - Object { - "alertDetailsUrl": "mockedAlertsLocator > getLocation", - "checkedAt": "July 6, 2020 9:14 PM", - "configId": undefined, - "latestErrorMessage": undefined, - "monitorId": "foo", - "monitorName": "Foo", - "monitorType": "myType", - "monitorUrl": "https://foo.com", - "observerHostname": undefined, - "observerLocation": "fairbanks", - "reason": "Monitor \\"Foo\\" from fairbanks 35 days availability is 98.03%. Alert when < 99.34%. Checked at July 6, 2020 9:14 PM.", - "statusMessage": "35 days availability is 98.03%. Alert when < 99.34%.", - "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zm9v?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", - }, - ], - Array [ - "xpack.uptime.alerts.actionGroups.monitorStatus", - Object { - "alertDetailsUrl": "mockedAlertsLocator > getLocation", - "checkedAt": "July 6, 2020 9:14 PM", - "configId": undefined, - "latestErrorMessage": undefined, - "monitorId": "unreliable", - "monitorName": "Unreliable", - "monitorType": "myType", - "monitorUrl": "https://unreliable.co", - "observerHostname": undefined, - "observerLocation": "fairbanks", - "reason": "Monitor \\"Unreliable\\" from fairbanks 35 days availability is 90.92%. Alert when < 99.34%. Checked at July 6, 2020 9:14 PM.", - "statusMessage": "35 days availability is 90.92%. Alert when < 99.34%.", - "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/dW5yZWxpYWJsZQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", - }, - ], - Array [ - "xpack.uptime.alerts.actionGroups.monitorStatus", - Object { - "alertDetailsUrl": "mockedAlertsLocator > getLocation", - "checkedAt": "July 6, 2020 9:14 PM", - "configId": undefined, - "latestErrorMessage": undefined, - "monitorId": "no-name", - "monitorName": "no-name", - "monitorType": "myType", - "monitorUrl": "https://no-name.co", - "observerHostname": undefined, - "observerLocation": "fairbanks", - "reason": "Monitor \\"no-name\\" from fairbanks 35 days availability is 90.92%. Alert when < 99.34%. Checked at July 6, 2020 9:14 PM.", - "statusMessage": "35 days availability is 90.92%. Alert when < 99.34%.", - "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/bm8tbmFtZQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", - }, - ], - ] + }) + ); + + expect(alertsClient.setAlertData).toHaveBeenCalledTimes(4); + expect(alertsClient.setAlertData.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "context": Object { + "alertDetailsUrl": "mockedAlertsLocator > getLocation", + "checkedAt": "July 6, 2020 9:14 PM", + "configId": undefined, + "latestErrorMessage": undefined, + "monitorId": "foo", + "monitorName": "Foo", + "monitorType": "myType", + "monitorUrl": "https://foo.com", + "observerHostname": undefined, + "observerLocation": "harrisburg", + "reason": "Monitor \\"Foo\\" from harrisburg 35 days availability is 99.28%. Alert when < 99.34%. Checked at July 6, 2020 9:14 PM.", + "statusMessage": "35 days availability is 99.28%. Alert when < 99.34%.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zm9v?dateRangeEnd=now&dateRangeStart=availability%20test&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", + }, + "id": "foo_https_foo_com_foo-harrisburg", + }, + ], + Array [ + Object { + "context": Object { + "alertDetailsUrl": "mockedAlertsLocator > getLocation", + "checkedAt": "July 6, 2020 9:14 PM", + "configId": undefined, + "latestErrorMessage": undefined, + "monitorId": "foo", + "monitorName": "Foo", + "monitorType": "myType", + "monitorUrl": "https://foo.com", + "observerHostname": undefined, + "observerLocation": "fairbanks", + "reason": "Monitor \\"Foo\\" from fairbanks 35 days availability is 98.03%. Alert when < 99.34%. Checked at July 6, 2020 9:14 PM.", + "statusMessage": "35 days availability is 98.03%. Alert when < 99.34%.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zm9v?dateRangeEnd=now&dateRangeStart=availability%20test&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", + }, + "id": "foo_https_foo_com_foo-fairbanks", + }, + ], + Array [ + Object { + "context": Object { + "alertDetailsUrl": "mockedAlertsLocator > getLocation", + "checkedAt": "July 6, 2020 9:14 PM", + "configId": undefined, + "latestErrorMessage": undefined, + "monitorId": "unreliable", + "monitorName": "Unreliable", + "monitorType": "myType", + "monitorUrl": "https://unreliable.co", + "observerHostname": undefined, + "observerLocation": "fairbanks", + "reason": "Monitor \\"Unreliable\\" from fairbanks 35 days availability is 90.92%. Alert when < 99.34%. Checked at July 6, 2020 9:14 PM.", + "statusMessage": "35 days availability is 90.92%. Alert when < 99.34%.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/dW5yZWxpYWJsZQ==?dateRangeEnd=now&dateRangeStart=availability%20test&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", + }, + "id": "unreliable_https_unreliable_co_unreliable-fairbanks", + }, + ], + Array [ + Object { + "context": Object { + "alertDetailsUrl": "mockedAlertsLocator > getLocation", + "checkedAt": "July 6, 2020 9:14 PM", + "configId": undefined, + "latestErrorMessage": undefined, + "monitorId": "no-name", + "monitorName": "no-name", + "monitorType": "myType", + "monitorUrl": "https://no-name.co", + "observerHostname": undefined, + "observerLocation": "fairbanks", + "reason": "Monitor \\"no-name\\" from fairbanks 35 days availability is 90.92%. Alert when < 99.34%. Checked at July 6, 2020 9:14 PM.", + "statusMessage": "35 days availability is 90.92%. Alert when < 99.34%.", + "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/bm8tbmFtZQ==?dateRangeEnd=now&dateRangeStart=availability%20test&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", + }, + "id": "https_no_name_co_no-name-fairbanks", + }, + ], + ] `); expect(mockGetter).not.toHaveBeenCalled(); expect(mockAvailability).toHaveBeenCalledTimes(1); @@ -1053,12 +1159,19 @@ describe('status check alert', () => { mockGetter.mockReturnValue(mockMonitors); const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter }); const alert = statusCheckAlertFactory(server, libs, plugins); - const options = mockOptions(); + const options = mockOptions(undefined, undefined, undefined, mockRecoveredAlerts); // @ts-ignore the executor can return `void`, but ours never does const state: Record = await alert.executor(options); - expect(options.setContext).toHaveBeenCalledTimes(2); - mockRecoveredAlerts.forEach((alertState) => { - expect(options.setContext).toHaveBeenCalledWith(alertState); + expect(options.services.alertsClient.setAlertData).toHaveBeenCalledTimes(4); + + expect(options.services.alertsClient.setAlertData).toHaveBeenNthCalledWith(3, { + context: mockRecoveredAlerts[0], + id: 'mock-id', + }); + + expect(options.services.alertsClient.setAlertData).toHaveBeenNthCalledWith(4, { + context: mockRecoveredAlerts[1], + id: 'mock-id', }); }); }); diff --git a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.ts b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.ts index cdce0d54b87d4..d9a7cd067d323 100644 --- a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.ts +++ b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/status_check.ts @@ -5,7 +5,7 @@ * 2.0. */ import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; -import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; +import { AlertsClientError, GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; import { min } from 'lodash'; import moment from 'moment'; @@ -354,18 +354,14 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( rule: { schedule: { interval }, }, - services: { - alertFactory, - alertWithLifecycle, - getAlertStartedDate, - getAlertUuid, - savedObjectsClient, - scopedClusterClient, - }, + services: { alertsClient, savedObjectsClient, scopedClusterClient }, spaceId, state, startedAt, }) { + if (!alertsClient) { + throw new AlertsClientError(); + } const { stackVersion = '8.9.0', availability, @@ -433,13 +429,23 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( const statusMessage = getStatusMessage(monitorStatusMessageParams); const monitorSummary = getMonitorSummary(monitorInfo, statusMessage); const alertId = getInstanceId(monitorInfo, monitorLoc.location); - const alert = alertWithLifecycle({ + const context = { + ...monitorSummary, + statusMessage, + }; + + const { uuid, start } = alertsClient.report({ id: alertId, - fields: getMonitorAlertDocument(monitorSummary), + actionGroup: MONITOR_STATUS.id, + payload: getMonitorAlertDocument(monitorSummary), + state: { + ...state, + ...context, + ...updateState(state, true), + }, }); - const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); - const alertUuid = getAlertUuid(alertId); + const indexedStartedAt = start ?? startedAt.toISOString(); const relativeViewInAppUrl = getMonitorRouteFromMonitorId({ monitorId: monitorSummary.monitorId, dateRangeEnd: 'now', @@ -449,37 +455,27 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( }, }); - const context = { - ...monitorSummary, - statusMessage, - }; - - alert.replaceState({ - ...state, - ...context, - ...updateState(state, true), - }); - - alert.scheduleActions(MONITOR_STATUS.id, { - [ALERT_DETAILS_URL]: await getAlertUrl( - alertUuid, - spaceId, - indexedStartedAt, - alertsLocator, - basePath.publicBaseUrl - ), - [VIEW_IN_APP_URL]: getViewInAppUrl(basePath, spaceId, relativeViewInAppUrl), - ...context, + alertsClient.setAlertData({ + id: alertId, + context: { + [ALERT_DETAILS_URL]: await getAlertUrl( + uuid, + spaceId, + indexedStartedAt, + alertsLocator, + basePath.publicBaseUrl + ), + [VIEW_IN_APP_URL]: getViewInAppUrl(basePath, spaceId, relativeViewInAppUrl), + ...context, + }, }); } - await setRecoveredAlertsContext({ - alertFactory, + await setRecoveredAlertsContext({ + alertsClient, alertsLocator, basePath, defaultStartedAt: startedAt.toISOString(), - getAlertStartedDate, - getAlertUuid, spaceId, }); @@ -528,23 +524,22 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( ); const monitorSummary = getMonitorSummary(monitorInfo, statusMessage); const alertId = getInstanceId(monitorInfo, monIdByLoc); - const alert = alertWithLifecycle({ - id: alertId, - fields: getMonitorAlertDocument(monitorSummary), - }); - const alertUuid = getAlertUuid(alertId); - const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); - const context = { ...monitorSummary, statusMessage, }; - alert.replaceState({ - ...updateState(state, true), - ...context, + const { uuid, start } = alertsClient.report({ + id: alertId, + actionGroup: MONITOR_STATUS.id, + payload: getMonitorAlertDocument(monitorSummary), + state: { + ...updateState(state, true), + ...context, + }, }); + const indexedStartedAt = start ?? startedAt.toISOString(); const relativeViewInAppUrl = getMonitorRouteFromMonitorId({ monitorId: monitorSummary.monitorId, dateRangeEnd: 'now', @@ -554,26 +549,27 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( }, }); - alert.scheduleActions(MONITOR_STATUS.id, { - [ALERT_DETAILS_URL]: await getAlertUrl( - alertUuid, - spaceId, - indexedStartedAt, - alertsLocator, - basePath.publicBaseUrl - ), - [VIEW_IN_APP_URL]: getViewInAppUrl(basePath, spaceId, relativeViewInAppUrl), - ...context, + alertsClient.setAlertData({ + id: alertId, + context: { + [ALERT_DETAILS_URL]: await getAlertUrl( + uuid, + spaceId, + indexedStartedAt, + alertsLocator, + basePath.publicBaseUrl + ), + [VIEW_IN_APP_URL]: getViewInAppUrl(basePath, spaceId, relativeViewInAppUrl), + ...context, + }, }); }); await setRecoveredAlertsContext({ - alertFactory, + alertsClient, alertsLocator, basePath, defaultStartedAt: startedAt.toISOString(), - getAlertStartedDate, - getAlertUuid, spaceId, }); diff --git a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/test_utils/index.ts b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/test_utils/index.ts index cfd7d26864fd3..f687584a95811 100644 --- a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/test_utils/index.ts +++ b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/test_utils/index.ts @@ -12,6 +12,7 @@ import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; import type { AlertsLocatorParams } from '@kbn/observability-plugin/common'; import { LocatorPublic } from '@kbn/share-plugin/common'; import { SharePluginSetup } from '@kbn/share-plugin/server'; +import { publicAlertsClientMock } from '@kbn/alerting-plugin/server/alerts_client/alerts_client.mock'; import { UMServerLibs } from '../../lib'; import { UptimeCorePluginsSetup, UptimeServerSetup } from '../../adapters'; import { getUptimeESMockClient } from '../../requests/test_helpers'; @@ -84,6 +85,7 @@ export const createRuleTypeMocks = (recoveredAlerts: Array> getAlertStartedDate: jest.fn().mockReturnValue('2022-03-17T13:13:33.755Z'), getAlertUuid: jest.fn().mockReturnValue('mock-alert-uuid'), logger: loggerMock, + alertsClient: publicAlertsClientMock.create(), }; return { diff --git a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/tls.test.ts b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/tls.test.ts index b44a358721490..867b03335594f 100644 --- a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/tls.test.ts +++ b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/tls.test.ts @@ -6,7 +6,6 @@ */ import moment from 'moment'; import { tlsAlertFactory, getCertSummary } from './tls'; -import { TLS } from '../../../../common/constants/uptime_alerts'; import { CertResult } from '../../../../common/runtime_types'; import { createRuleTypeMocks, bootstrapDependencies } from './test_utils'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; @@ -60,6 +59,7 @@ const mockCertResult: CertResult = { const mockRecoveredAlerts = [ { + id: 'recovered-1', alertDetailsUrl: 'mockedAlertsLocator > getLocation', commonName: mockCertResult.certs[0].common_name ?? '', issuer: mockCertResult.certs[0].issuer ?? '', @@ -67,6 +67,7 @@ const mockRecoveredAlerts = [ status: 'expired', }, { + id: 'recovered-2', alertDetailsUrl: 'mockedAlertsLocator > getLocation', commonName: mockCertResult.certs[1].common_name ?? '', issuer: mockCertResult.certs[1].issuer ?? '', @@ -75,12 +76,33 @@ const mockRecoveredAlerts = [ }, ]; -const mockOptions = (state = {}): any => { +const mockOptions = (state = {}, recoveredAlerts: typeof mockRecoveredAlerts = []): any => { const { services, setContext } = createRuleTypeMocks(mockRecoveredAlerts); const params = { timerange: { from: 'now-15m', to: 'now' }, }; + services.alertsClient.report.mockImplementation((param: any) => { + return { + uuid: `uuid-${param.id}`, + start: new Date().toISOString(), + alertDoc: {}, + }; + }); + + services.alertsClient.getRecoveredAlerts.mockImplementation((param: any) => { + return recoveredAlerts.map((alert) => ({ + alert: { + getId: () => alert.id, + getUuid: () => 'mock-uuid', + getState: () => alert, + getStart: () => new Date().toISOString(), + setContext, + context: {}, + }, + })); + }); + return { params, state, @@ -115,23 +137,40 @@ describe('tls alert', () => { const alert = tlsAlertFactory(server, libs, plugins); const options = mockOptions(); const { - services: { alertWithLifecycle }, + services: { alertsClient }, } = options; await alert.executor(options); expect(mockGetter).toHaveBeenCalledTimes(1); - expect(alertWithLifecycle).toHaveBeenCalledTimes(4); + expect(alertsClient.report).toHaveBeenCalledTimes(4); mockCertResult.certs.forEach((cert) => { - expect(alertWithLifecycle).toBeCalledWith({ - fields: expect.objectContaining({ + const context = { + commonName: cert.common_name, + issuer: cert.issuer, + status: 'expired', + }; + + expect(alertsClient.report).toBeCalledWith({ + id: `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`, + actionGroup: 'xpack.uptime.alerts.actionGroups.tlsCertificate', + state: expect.objectContaining(context), + }); + + expect(alertsClient.setAlertData).toBeCalledWith({ + id: `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`, + context: expect.objectContaining(context), + payload: expect.objectContaining({ 'tls.server.x509.subject.common_name': cert.common_name, 'tls.server.x509.issuer.common_name': cert.issuer, 'tls.server.x509.not_after': cert.not_after, 'tls.server.x509.not_before': cert.not_before, 'tls.server.hash.sha256': cert.sha256, }), - id: `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`, }); }); + + expect(alertsClient.report).toHaveBeenCalledTimes(4); + expect(alertsClient.setAlertData).toHaveBeenCalledTimes(4); + expect(mockGetter).toBeCalledWith( expect.objectContaining({ pageIndex: 0, @@ -142,21 +181,6 @@ describe('tls alert', () => { direction: 'desc', }) ); - const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; - expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(4); - mockCertResult.certs.forEach((cert) => { - const context = { - commonName: cert.common_name, - issuer: cert.issuer, - status: 'expired', - }; - expect(alertInstanceMock.replaceState).toBeCalledWith(expect.objectContaining(context)); - expect(alertInstanceMock.scheduleActions).toBeCalledWith( - TLS.id, - expect.objectContaining(context) - ); - }); - expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(4); }); it('does not trigger when cert is not considered aging or expiring', async () => { @@ -204,11 +228,11 @@ describe('tls alert', () => { const alert = tlsAlertFactory(server, libs, plugins); const options = mockOptions(); const { - services: { alertWithLifecycle }, + services: { alertsClient }, } = options; await alert.executor(options); expect(mockGetter).toHaveBeenCalledTimes(1); - expect(alertWithLifecycle).toHaveBeenCalledTimes(0); + expect(alertsClient.report).toHaveBeenCalledTimes(0); expect(mockGetter).toBeCalledWith( expect.objectContaining({ pageIndex: 0, @@ -253,12 +277,18 @@ describe('tls alert', () => { mockGetter.mockReturnValue(mockCertResult); const { server, libs, plugins } = bootstrapDependencies({ getCerts: mockGetter }); const alert = tlsAlertFactory(server, libs, plugins); - const options = mockOptions(); + const options = mockOptions(undefined, mockRecoveredAlerts); // @ts-ignore the executor can return `void`, but ours never does - const state: Record = await alert.executor(options); - expect(options.setContext).toHaveBeenCalledTimes(2); - mockRecoveredAlerts.forEach((alertState) => { - expect(options.setContext).toHaveBeenCalledWith(alertState); + const { + services: { alertsClient }, + } = options; + await alert.executor(options); + expect(alertsClient.setAlertData).toHaveBeenCalledTimes(6); + mockRecoveredAlerts.forEach((recoveredAlert) => { + expect(alertsClient.setAlertData).toHaveBeenCalledWith({ + id: recoveredAlert.id, + context: recoveredAlert, + }); }); }); }); diff --git a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/tls.ts b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/tls.ts index 40d16ad9a6289..560ca9c854d78 100644 --- a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/tls.ts +++ b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/tls.ts @@ -6,7 +6,7 @@ */ import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; -import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; +import { AlertsClientError, GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; import moment from 'moment'; import { ActionGroupIdsOf } from '@kbn/alerting-plugin/common'; import { schema } from '@kbn/config-schema'; @@ -149,19 +149,15 @@ export const tlsAlertFactory: UptimeAlertTypeFactory = ( doesSetRecoveryContext: true, async executor({ params, - services: { - alertFactory, - alertWithLifecycle, - getAlertStartedDate, - getAlertUuid, - savedObjectsClient, - scopedClusterClient, - }, + services: { alertsClient, savedObjectsClient, scopedClusterClient }, spaceId, startedAt, state, rule, }) { + if (!alertsClient) { + throw new AlertsClientError(); + } const { share, basePath } = _server; const alertsLocator: LocatorPublic | undefined = share.url.locators.get(alertsLocatorID); @@ -215,47 +211,48 @@ export const tlsAlertFactory: UptimeAlertTypeFactory = ( } const alertId = `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`; - const alertUuid = getAlertUuid(alertId); - const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString(); - const alertInstance = alertWithLifecycle({ + const { uuid, start } = alertsClient.report({ id: alertId, - fields: { + actionGroup: TLS.id, + state: { + ...updateState(state, foundCerts), + ...summary, + }, + }); + + const indexedStartedAt = start ?? startedAt.toISOString(); + + alertsClient.setAlertData({ + id: alertId, + context: { + [ALERT_DETAILS_URL]: await getAlertUrl( + uuid, + spaceId, + indexedStartedAt, + alertsLocator, + basePath.publicBaseUrl + ), + ...summary, + }, + payload: { 'tls.server.x509.subject.common_name': cert.common_name, 'tls.server.x509.issuer.common_name': cert.issuer, 'tls.server.x509.not_after': cert.not_after, 'tls.server.x509.not_before': cert.not_before, 'tls.server.hash.sha256': cert.sha256, [ALERT_REASON]: generateAlertMessage(TlsTranslations.defaultActionMessage, summary), - [ALERT_UUID]: alertUuid, + [ALERT_UUID]: uuid, }, }); - - alertInstance.replaceState({ - ...updateState(state, foundCerts), - ...summary, - }); - - alertInstance.scheduleActions(TLS.id, { - [ALERT_DETAILS_URL]: await getAlertUrl( - alertUuid, - spaceId, - indexedStartedAt, - alertsLocator, - basePath.publicBaseUrl - ), - ...summary, - }); }); } - await setRecoveredAlertsContext({ - alertFactory, + await setRecoveredAlertsContext({ + alertsClient, alertsLocator, basePath, defaultStartedAt: startedAt.toISOString(), - getAlertStartedDate, - getAlertUuid, spaceId, }); diff --git a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/types.ts b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/types.ts index 1adefba8f9154..2eaa7dbabb278 100644 --- a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/types.ts +++ b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/lib/alerts/types.ts @@ -4,15 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { AlertTypeWithExecutor } from '@kbn/rule-registry-plugin/server'; import { AlertInstanceContext, AlertInstanceState, RecoveredActionGroupId, } from '@kbn/alerting-plugin/common'; import { RuleType } from '@kbn/alerting-plugin/server'; -import { LifecycleAlertServices } from '@kbn/rule-registry-plugin/server'; -import { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { DefaultAlert, ObservabilityUptimeAlert } from '@kbn/alerts-as-data-utils'; import { UMServerLibs } from '../lib'; import { UptimeCorePluginsSetup, UptimeServerSetup } from '../adapters'; @@ -22,11 +20,15 @@ import { UptimeCorePluginsSetup, UptimeServerSetup } from '../adapters'; * * When we register all the alerts we can inject this field. */ -export type DefaultUptimeAlertInstance = AlertTypeWithExecutor< +export type DefaultUptimeAlertInstance = RuleType< Record, + never, Record, + AlertInstanceState, AlertInstanceContext, - LifecycleAlertServices, AlertInstanceContext, TActionGroupIds> + TActionGroupIds, + RecoveredActionGroupId, + ObservabilityUptimeAlert >; export type UptimeAlertTypeFactory = ( diff --git a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/uptime_server.ts b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/uptime_server.ts index 528de02bb4ea5..0c417a6e061e6 100644 --- a/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/uptime_server.ts +++ b/x-pack/plugins/observability_solution/uptime/server/legacy_uptime/uptime_server.ts @@ -6,7 +6,7 @@ */ import { Logger } from '@kbn/core/server'; -import { createLifecycleRuleTypeFactory, IRuleDataClient } from '@kbn/rule-registry-plugin/server'; +import { IRuleDataClient } from '@kbn/rule-registry-plugin/server'; import { getRequestValidation } from '@kbn/core-http-server'; import { INITIAL_REST_VERSION } from '../../common/constants'; import { DynamicSettingsSchema } from './routes/dynamic_settings'; @@ -151,14 +151,9 @@ export const initUptimeServer = ( const tlsAlert = tlsAlertFactory(server, libs, plugins); const durationAlert = durationAnomalyAlertFactory(server, libs, plugins); - const createLifecycleRuleType = createLifecycleRuleTypeFactory({ - ruleDataClient, - logger, - }); - - registerType(createLifecycleRuleType(statusAlert)); - registerType(createLifecycleRuleType(tlsAlert)); - registerType(createLifecycleRuleType(durationAlert)); + registerType(statusAlert); + registerType(tlsAlert); + registerType(durationAlert); /* TLS Legacy rule supported at least through 8.0. * Not registered with RAC */ diff --git a/x-pack/plugins/security/server/build_delegate_apis.test.ts b/x-pack/plugins/security/server/build_delegate_apis.test.ts index 9511110cdc173..59963ad2aef8e 100644 --- a/x-pack/plugins/security/server/build_delegate_apis.test.ts +++ b/x-pack/plugins/security/server/build_delegate_apis.test.ts @@ -6,9 +6,10 @@ */ import { httpServerMock } from '@kbn/core-http-server-mocks'; -import type { CoreSecurityDelegateContract } from '@kbn/core-security-server'; +import type { AuditLogger, CoreSecurityDelegateContract } from '@kbn/core-security-server'; import type { CoreUserProfileDelegateContract } from '@kbn/core-user-profile-server'; +import { auditServiceMock } from './audit/mocks'; import { authenticationServiceMock } from './authentication/authentication_service.mock'; import { buildSecurityApi, buildUserProfileApi } from './build_delegate_apis'; import { securityMock } from './mocks'; @@ -16,11 +17,13 @@ import { userProfileServiceMock } from './user_profile/user_profile_service.mock describe('buildSecurityApi', () => { let authc: ReturnType; + let auditService: ReturnType; let api: CoreSecurityDelegateContract; beforeEach(() => { authc = authenticationServiceMock.createStart(); - api = buildSecurityApi({ getAuthc: () => authc }); + auditService = auditServiceMock.create(); + api = buildSecurityApi({ getAuthc: () => authc, audit: auditService }); }); describe('authc.getCurrentUser', () => { @@ -43,6 +46,25 @@ describe('buildSecurityApi', () => { expect(currentUser).toBe(delegateReturn); }); }); + + describe('audit.asScoped', () => { + let auditLogger: AuditLogger; + it('properly delegates to the service', () => { + const request = httpServerMock.createKibanaRequest(); + auditLogger = api.audit.asScoped(request); + auditLogger.log({ message: 'an event' }); + expect(auditService.asScoped).toHaveBeenCalledTimes(1); + expect(auditService.asScoped).toHaveBeenCalledWith(request); + }); + + it('returns the result from the service', async () => { + const request = httpServerMock.createKibanaRequest(); + auditLogger = api.audit.asScoped(request); + auditLogger.log({ message: 'an event' }); + expect(auditService.asScoped(request).log).toHaveBeenCalledTimes(1); + expect(auditService.asScoped(request).log).toHaveBeenCalledWith({ message: 'an event' }); + }); + }); }); describe('buildUserProfileApi', () => { diff --git a/x-pack/plugins/security/server/build_delegate_apis.ts b/x-pack/plugins/security/server/build_delegate_apis.ts index b4fd4474aaace..fb782f3db256f 100644 --- a/x-pack/plugins/security/server/build_delegate_apis.ts +++ b/x-pack/plugins/security/server/build_delegate_apis.ts @@ -7,14 +7,17 @@ import type { CoreSecurityDelegateContract } from '@kbn/core-security-server'; import type { CoreUserProfileDelegateContract } from '@kbn/core-user-profile-server'; +import type { AuditServiceSetup } from '@kbn/security-plugin-types-server'; import type { InternalAuthenticationServiceStart } from './authentication'; import type { UserProfileServiceStartInternal } from './user_profile'; export const buildSecurityApi = ({ getAuthc, + audit, }: { getAuthc: () => InternalAuthenticationServiceStart; + audit: AuditServiceSetup; }): CoreSecurityDelegateContract => { return { authc: { @@ -22,6 +25,15 @@ export const buildSecurityApi = ({ return getAuthc().getCurrentUser(request); }, }, + audit: { + asScoped(request) { + return audit.asScoped(request); + }, + withoutRequest: { + log: audit.withoutRequest.log, + enabled: audit.withoutRequest.enabled, + }, + }, }; }; diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 7153e1cc6794e..791d784c36a0d 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -298,6 +298,7 @@ export class SecurityPlugin core.security.registerSecurityDelegate( buildSecurityApi({ getAuthc: this.getAuthentication.bind(this), + audit: this.auditSetup, }) ); core.userProfile.registerUserProfileDelegate( diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts index c2ed7dd7061b5..7a3caa983f984 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen.ts @@ -26,6 +26,7 @@ import { RuleActionAlertsFilter, IndexPatternArray, RuleTagArray, + InvestigationFields, TimelineTemplateId, TimelineTemplateTitle, } from '../../model/rule_schema/common_attributes.gen'; @@ -52,6 +53,7 @@ export const BulkActionsDryRunErrCode = z.enum([ 'MACHINE_LEARNING_AUTH', 'MACHINE_LEARNING_INDEX_PATTERN', 'ESQL_INDEX_PATTERN', + 'INVESTIGATION_FIELDS_FEATURE', ]); export type BulkActionsDryRunErrCodeEnum = typeof BulkActionsDryRunErrCode.enum; export const BulkActionsDryRunErrCodeEnum = BulkActionsDryRunErrCode.enum; @@ -187,6 +189,9 @@ export const BulkActionEditType = z.enum([ 'add_rule_actions', 'set_rule_actions', 'set_schedule', + 'add_investigation_fields', + 'delete_investigation_fields', + 'set_investigation_fields', ]); export type BulkActionEditTypeEnum = typeof BulkActionEditType.enum; export const BulkActionEditTypeEnum = BulkActionEditType.enum; @@ -239,6 +244,18 @@ export const BulkActionEditPayloadTags = z.object({ value: RuleTagArray, }); +export type BulkActionEditPayloadInvestigationFields = z.infer< + typeof BulkActionEditPayloadInvestigationFields +>; +export const BulkActionEditPayloadInvestigationFields = z.object({ + type: z.enum([ + 'add_investigation_fields', + 'delete_investigation_fields', + 'set_investigation_fields', + ]), + value: InvestigationFields, +}); + export type BulkActionEditPayloadTimeline = z.infer; export const BulkActionEditPayloadTimeline = z.object({ type: z.literal('set_timeline'), @@ -252,6 +269,7 @@ export type BulkActionEditPayload = z.infer; export const BulkActionEditPayload = z.union([ BulkActionEditPayloadTags, BulkActionEditPayloadIndexPatterns, + BulkActionEditPayloadInvestigationFields, BulkActionEditPayloadTimeline, BulkActionEditPayloadRuleActions, BulkActionEditPayloadSchedule, diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml index 10422772785e3..6b5a3aa1c6982 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml @@ -76,6 +76,7 @@ components: - MACHINE_LEARNING_AUTH - MACHINE_LEARNING_INDEX_PATTERN - ESQL_INDEX_PATTERN + - INVESTIGATION_FIELDS_FEATURE NormalizedRuleError: type: object @@ -281,6 +282,9 @@ components: - add_rule_actions - set_rule_actions - set_schedule + - add_investigation_fields + - delete_investigation_fields + - set_investigation_fields # Per rulesClient.bulkEdit rules actions operation contract (x-pack/plugins/alerting/server/rules_client/rules_client.ts) normalized rule action object is expected (NormalizedAlertAction) as value for the edit operation NormalizedRuleAction: @@ -381,6 +385,21 @@ components: - type - value + BulkActionEditPayloadInvestigationFields: + type: object + properties: + type: + type: string + enum: + - add_investigation_fields + - delete_investigation_fields + - set_investigation_fields + value: + $ref: '../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/InvestigationFields' + required: + - type + - value + BulkActionEditPayloadTimeline: type: object properties: @@ -406,6 +425,7 @@ components: anyOf: - $ref: '#/components/schemas/BulkActionEditPayloadTags' - $ref: '#/components/schemas/BulkActionEditPayloadIndexPatterns' + - $ref: '#/components/schemas/BulkActionEditPayloadInvestigationFields' - $ref: '#/components/schemas/BulkActionEditPayloadTimeline' - $ref: '#/components/schemas/BulkActionEditPayloadRuleActions' - $ref: '#/components/schemas/BulkActionEditPayloadSchedule' diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.test.ts index ff5289f79d98d..74bdf8707629e 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.test.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.test.ts @@ -187,7 +187,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 11 more"` ); }); @@ -249,7 +249,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 11 more"` ); }); @@ -299,6 +299,62 @@ describe('Perform bulk action request schema', () => { }); }); + describe('investigation_fields', () => { + test('valid request: set_investigation_fields edit action', () => { + const payload: PerformBulkActionRequestBody = { + query: 'name: test', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.set_investigation_fields, + value: { field_names: ['field-1'] }, + }, + ], + }; + + const result = PerformBulkActionRequestBody.safeParse(payload); + + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + + test('valid request: add_investigation_fields edit action', () => { + const payload: PerformBulkActionRequestBody = { + query: 'name: test', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.add_investigation_fields, + value: { field_names: ['field-2'] }, + }, + ], + }; + + const result = PerformBulkActionRequestBody.safeParse(payload); + + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + + test('valid request: delete_investigation_fields edit action', () => { + const payload: PerformBulkActionRequestBody = { + query: 'name: test', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.delete_investigation_fields, + value: { field_names: ['field-3'] }, + }, + ], + }; + + const result = PerformBulkActionRequestBody.safeParse(payload); + + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); + }); + describe('timeline', () => { test('invalid request: wrong timeline payload type', () => { const payload = { @@ -311,7 +367,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 7 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"` ); }); @@ -333,7 +389,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 10 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 12 more"` ); }); @@ -371,7 +427,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 7 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"` ); }); @@ -416,7 +472,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 10 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 12 more"` ); }); @@ -438,7 +494,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 10 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 12 more"` ); }); @@ -476,7 +532,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 7 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 9 more"` ); }); @@ -498,7 +554,7 @@ describe('Perform bulk action request schema', () => { expectParseError(result); expect(stringifyZodError(result.error)).toMatchInlineSnapshot( - `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 11 more"` + `"action: Invalid literal value, expected \\"delete\\", action: Invalid literal value, expected \\"disable\\", action: Invalid literal value, expected \\"enable\\", action: Invalid literal value, expected \\"export\\", action: Invalid literal value, expected \\"duplicate\\", and 13 more"` ); }); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_types.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_types.ts index 6e57e5abe2410..c160b6fb21c27 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_types.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_types.ts @@ -7,6 +7,7 @@ import type { BulkActionEditPayloadIndexPatterns, + BulkActionEditPayloadInvestigationFields, BulkActionEditPayloadRuleActions, BulkActionEditPayloadSchedule, BulkActionEditPayloadTags, @@ -26,5 +27,6 @@ export type BulkActionEditForRuleAttributes = */ export type BulkActionEditForRuleParams = | BulkActionEditPayloadIndexPatterns + | BulkActionEditPayloadInvestigationFields | BulkActionEditPayloadTimeline | BulkActionEditPayloadSchedule; diff --git a/x-pack/plugins/security_solution/common/config_settings.ts b/x-pack/plugins/security_solution/common/config_settings.ts index 5d5c40fa2b48c..6dd9967f6e3fd 100644 --- a/x-pack/plugins/security_solution/common/config_settings.ts +++ b/x-pack/plugins/security_solution/common/config_settings.ts @@ -10,10 +10,6 @@ export interface ConfigSettings { * Index Lifecycle Management (ILM) feature enabled. */ ILMEnabled: boolean; - /** - * ESQL queries enabled. - */ - ESQLEnabled: boolean; } /** @@ -22,7 +18,6 @@ export interface ConfigSettings { */ export const defaultSettings: ConfigSettings = Object.freeze({ ILMEnabled: true, - ESQLEnabled: true, }); type ConfigSettingsKey = keyof ConfigSettings; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 0a470b0d234f3..ceff046d8c678 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -442,6 +442,7 @@ export enum BulkActionsDryRunErrCode { MACHINE_LEARNING_AUTH = 'MACHINE_LEARNING_AUTH', MACHINE_LEARNING_INDEX_PATTERN = 'MACHINE_LEARNING_INDEX_PATTERN', ESQL_INDEX_PATTERN = 'ESQL_INDEX_PATTERN', + INVESTIGATION_FIELDS_FEATURE = 'INVESTIGATION_FIELDS_FEATURE', } export const MAX_NUMBER_OF_NEW_TERMS_FIELDS = 3; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index f575a6ffcd091..175a40288b9d8 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -261,6 +261,11 @@ export const allowedExperimentalValues = Object.freeze({ * Enables the new modal for the value list items */ valueListItemsModalEnabled: true, + + /** + * Enables the new rule's bulk action to manage custom highlighted fields + */ + bulkCustomHighlightedFieldsEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/public/common/components/hooks/index.ts b/x-pack/plugins/security_solution/public/common/components/hooks/index.ts deleted file mode 100644 index ba849f2c85864..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/hooks/index.ts +++ /dev/null @@ -1,8 +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. - */ - -export { useIsEsqlRuleTypeEnabled } from './use_is_esql_rule_type_enabled'; diff --git a/x-pack/plugins/security_solution/public/common/components/hooks/use_is_esql_rule_type_enabled.ts b/x-pack/plugins/security_solution/public/common/components/hooks/use_is_esql_rule_type_enabled.ts deleted file mode 100644 index 239c49088e644..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/hooks/use_is_esql_rule_type_enabled.ts +++ /dev/null @@ -1,16 +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 { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; -import { useKibana } from '../../lib/kibana'; - -export const useIsEsqlRuleTypeEnabled = (): boolean => { - const isEsqlSettingEnabled = useKibana().services.configSettings.ESQLEnabled; - const isEsqlRuleTypeEnabled = !useIsExperimentalFeatureEnabled('esqlRulesDisabled'); - - return isEsqlSettingEnabled && isEsqlRuleTypeEnabled; -}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/esql/use_esql_availability.ts b/x-pack/plugins/security_solution/public/common/hooks/esql/use_esql_availability.ts new file mode 100644 index 0000000000000..41fc7084b32bf --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/esql/use_esql_availability.ts @@ -0,0 +1,34 @@ +/* + * 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 { useMemo } from 'react'; +import { ENABLE_ESQL } from '@kbn/esql-utils'; +import { useKibana } from '../../lib/kibana'; +import { useIsExperimentalFeatureEnabled } from '../use_experimental_features'; + +/** + * This hook combines the checks for esql availability within the security solution + * If the advanced setting is disabled, ESQL will not be accessible in the UI for any new timeline or new rule creation workflows + * The feature flags are still available to provide users an escape hatch in case of any esql related performance issues + */ +export const useEsqlAvailability = () => { + const { uiSettings } = useKibana().services; + const isEsqlAdvancedSettingEnabled = uiSettings?.get(ENABLE_ESQL); + const isEsqlRuleTypeEnabled = + !useIsExperimentalFeatureEnabled('esqlRulesDisabled') && isEsqlAdvancedSettingEnabled; + const isESQLTabInTimelineEnabled = + !useIsExperimentalFeatureEnabled('timelineEsqlTabDisabled') && isEsqlAdvancedSettingEnabled; + + return useMemo( + () => ({ + isEsqlAdvancedSettingEnabled, + isEsqlRuleTypeEnabled, + isESQLTabInTimelineEnabled, + }), + [isESQLTabInTimelineEnabled, isEsqlAdvancedSettingEnabled, isEsqlRuleTypeEnabled] + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts index a6eff07ac00ff..b7893592296e9 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts @@ -41,6 +41,11 @@ export enum TELEMETRY_EVENT { DELETE_VALUE_LIST_ITEM = 'delete_value_list_item', EDIT_VALUE_LIST_ITEM = 'edit_value_list_item', ADDITIONAL_UPLOAD_VALUE_LIST_ITEM = 'additinonal_upload_value_list_item', + + // Bulk custom highlighted fields action + ADD_INVESTIGATION_FIELDS = 'add_investigation_fields', + SET_INVESTIGATION_FIELDS = 'set_investigation_fields', + DELETE_INVESTIGATION_FIELDS = 'delete_investigation_fields', } export enum TelemetryEventTypes { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.test.tsx index 73bdf48623e2a..9930dc7f626ad 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.test.tsx @@ -10,12 +10,12 @@ import { mount, shallow } from 'enzyme'; import { SelectRuleType } from '.'; import { TestProviders, useFormFieldMock } from '../../../../common/mock'; -import { useIsEsqlRuleTypeEnabled } from '../../../../common/components/hooks'; +import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability'; -jest.mock('../../../../common/components/hooks', () => ({ - useIsEsqlRuleTypeEnabled: jest.fn().mockReturnValue(true), +jest.mock('../../../../common/hooks/esql/use_esql_availability', () => ({ + useEsqlAvailability: jest.fn().mockReturnValue({ isEsqlRuleTypeEnabled: true }), })); -const useIsEsqlRuleTypeEnabledMock = useIsEsqlRuleTypeEnabled as jest.Mock; +const useEsqlAvailabilityMock = useEsqlAvailability as jest.Mock; describe('SelectRuleType', () => { it('renders correctly', () => { @@ -185,8 +185,30 @@ describe('SelectRuleType', () => { expect(wrapper.find('[data-test-subj="esqlRuleType"]').exists()).toBeTruthy(); }); + it('renders selected card only when in update mode for "esql" and esql feature is disabled', () => { + useEsqlAvailabilityMock.mockReturnValueOnce(false); + const field = useFormFieldMock({ value: 'esql' }); + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="customRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="machineLearningRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="eqlRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="threatMatchRuleType"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="esqlRuleType"]').exists()).toBeTruthy(); + }); + it('should not render "esql" rule type if esql rule is not enabled', () => { - useIsEsqlRuleTypeEnabledMock.mockReturnValueOnce(false); + useEsqlAvailabilityMock.mockReturnValueOnce(false); const Component = () => { const field = useFormFieldMock(); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.tsx index c93b107849c2b..2b222fd3c393f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/select_rule_type/index.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useMemo, memo } from 'react'; import { EuiCard, EuiFlexGrid, EuiFlexItem, EuiFormRow, EuiIcon } from '@elastic/eui'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { isThresholdRule, @@ -21,7 +22,6 @@ import { import type { FieldHook } from '../../../../shared_imports'; import * as i18n from './translations'; import { MlCardDescription } from './ml_card_description'; -import { useIsEsqlRuleTypeEnabled } from '../../../../common/components/hooks'; interface SelectRuleTypeProps { describedByIds: string[]; @@ -48,7 +48,7 @@ export const SelectRuleType: React.FC = memo( const setNewTerms = useCallback(() => setType('new_terms'), [setType]); const setEsql = useCallback(() => setType('esql'), [setType]); - const isEsqlRuleTypeEnabled = useIsEsqlRuleTypeEnabled(); + const { isEsqlRuleTypeEnabled } = useEsqlAvailability(); const eqlSelectableConfig = useMemo( () => ({ @@ -194,7 +194,7 @@ export const SelectRuleType: React.FC = memo( /> )} - {isEsqlRuleTypeEnabled && (!isUpdateView || esqlSelectableConfig.isSelected) && ( + {((!isUpdateView && isEsqlRuleTypeEnabled) || esqlSelectableConfig.isSelected) && ( void; @@ -38,6 +39,11 @@ const BulkEditFlyoutComponent = ({ editAction, ...props }: BulkEditFlyoutProps) case BulkActionEditTypeEnum.set_tags: return ; + case BulkActionEditTypeEnum.add_investigation_fields: + case BulkActionEditTypeEnum.delete_investigation_fields: + case BulkActionEditTypeEnum.set_investigation_fields: + return ; + case BulkActionEditTypeEnum.set_timeline: return ; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/investigation_fields_form.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/investigation_fields_form.tsx new file mode 100644 index 0000000000000..449664e20222a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/investigation_fields_form.tsx @@ -0,0 +1,179 @@ +/* + * 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 { EuiFormRow, EuiCallOut } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { useKibana } from '../../../../../../common/lib/kibana'; +import { DEFAULT_INDEX_KEY } from '../../../../../../../common/constants'; +import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../../../../../common/lib/telemetry'; +import * as i18n from '../../../../../../detections/pages/detection_engine/rules/translations'; + +import { useFetchIndex } from '../../../../../../common/containers/source'; + +import { BulkActionEditTypeEnum } from '../../../../../../../common/api/detection_engine/rule_management'; +import type { BulkActionEditPayload } from '../../../../../../../common/api/detection_engine/rule_management'; + +import type { FormSchema } from '../../../../../../shared_imports'; +import { + Field, + getUseField, + useFormData, + useForm, + FIELD_TYPES, + fieldValidators, +} from '../../../../../../shared_imports'; + +import { BulkEditFormWrapper } from './bulk_edit_form_wrapper'; + +const CommonUseField = getUseField({ component: Field }); + +type InvestigationFieldsEditActions = + | BulkActionEditTypeEnum['add_investigation_fields'] + | BulkActionEditTypeEnum['delete_investigation_fields'] + | BulkActionEditTypeEnum['set_investigation_fields']; + +interface InvestigationFieldsFormData { + investigationFields: string[]; + overwrite: boolean; +} + +const schema: FormSchema = { + investigationFields: { + fieldsToValidateOnChange: ['investigationFields'], + type: FIELD_TYPES.COMBO_BOX, + validations: [ + { + validator: fieldValidators.emptyField( + i18n.BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_REQUIRED_ERROR + ), + }, + ], + }, + overwrite: { + type: FIELD_TYPES.CHECKBOX, + label: i18n.BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_OVERWRITE_LABEL, + }, +}; + +const initialFormData: InvestigationFieldsFormData = { + investigationFields: [], + overwrite: false, +}; + +const getFormConfig = (editAction: InvestigationFieldsEditActions) => + editAction === BulkActionEditTypeEnum.add_investigation_fields + ? { + indexLabel: i18n.BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_LABEL, + indexHelpText: i18n.BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_HELP_TEXT, + formTitle: i18n.BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_TITLE, + } + : { + indexLabel: i18n.BULK_EDIT_FLYOUT_FORM_DELETE_INVESTIGATION_FIELDS_LABEL, + indexHelpText: i18n.BULK_EDIT_FLYOUT_FORM_DELETE_INVESTIGATION_FIELDS_HELP_TEXT, + formTitle: i18n.BULK_EDIT_FLYOUT_FORM_DELETE_INVESTIGATION_FIELDS_TITLE, + }; + +interface InvestigationFieldsFormProps { + editAction: InvestigationFieldsEditActions; + rulesCount: number; + onClose: () => void; + onConfirm: (bulkActionEditPayload: BulkActionEditPayload) => void; +} + +const InvestigationFieldsFormComponent = ({ + editAction, + rulesCount, + onClose, + onConfirm, +}: InvestigationFieldsFormProps) => { + const { form } = useForm({ + defaultValue: initialFormData, + schema, + }); + + const { uiSettings } = useKibana().services; + const defaultPatterns = uiSettings.get(DEFAULT_INDEX_KEY); + + const { indexHelpText, indexLabel, formTitle } = getFormConfig(editAction); + + const [{ overwrite }] = useFormData({ + form, + watch: ['overwrite'], + }); + const [_, { indexPatterns }] = useFetchIndex(defaultPatterns, false); + const fieldOptions = indexPatterns.fields.map((field) => ({ + label: field.name, + })); + + const handleSubmit = async () => { + const { data, isValid } = await form.submit(); + if (!isValid) { + return; + } + + const event = data.overwrite + ? TELEMETRY_EVENT.SET_INVESTIGATION_FIELDS + : editAction === 'delete_investigation_fields' + ? TELEMETRY_EVENT.DELETE_INVESTIGATION_FIELDS + : TELEMETRY_EVENT.ADD_INVESTIGATION_FIELDS; + track(METRIC_TYPE.CLICK, event); + + onConfirm({ + value: { field_names: data.investigationFields }, + type: data.overwrite ? BulkActionEditTypeEnum.set_investigation_fields : editAction, + }); + }; + + return ( + + + {editAction === BulkActionEditTypeEnum.add_investigation_fields && ( + + )} + {overwrite && ( + + + + + + )} + + ); +}; + +export const InvestigationFieldsForm = React.memo(InvestigationFieldsFormComponent); +InvestigationFieldsForm.displayName = 'InvestigationFieldsForm'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx index 0e57febd33979..70d522323a964 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx @@ -12,6 +12,7 @@ import type { Toast } from '@kbn/core/public'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { euiThemeVars } from '@kbn/ui-theme'; import React, { useCallback } from 'react'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { useKibana } from '../../../../../common/lib/kibana'; import { convertRulesFilterToKQL } from '../../../../../../common/detection_engine/rule_management/rule_filtering'; import { DuplicateOptions } from '../../../../../../common/detection_engine/rule_management/constants'; @@ -82,6 +83,10 @@ export const useBulkActions = ({ actions: { clearRulesSelection, setIsPreflightInProgress }, } = rulesTableContext; + const isBulkCustomHighlightedFieldsEnabled = useIsExperimentalFeatureEnabled( + 'bulkCustomHighlightedFieldsEnabled' + ); + const getBulkItemsPopoverContent = useCallback( (closePopover: () => void): EuiContextMenuPanelDescriptor[] => { const selectedRules = rules.filter(({ id }) => selectedRuleIds.includes(id)); @@ -331,6 +336,17 @@ export const useBulkActions = ({ disabled: isEditDisabled, panel: 1, }, + ...(isBulkCustomHighlightedFieldsEnabled + ? [ + { + key: i18n.BULK_ACTION_INVESTIGATION_FIELDS, + name: i18n.BULK_ACTION_INVESTIGATION_FIELDS, + 'data-test-subj': 'investigationFieldsBulkEditRule', + disabled: isEditDisabled, + panel: 3, + }, + ] + : []), { key: i18n.BULK_ACTION_ADD_RULE_ACTIONS, name: i18n.BULK_ACTION_ADD_RULE_ACTIONS, @@ -461,6 +477,34 @@ export const useBulkActions = ({ }, ], }, + { + id: 3, + title: i18n.BULK_ACTION_MENU_TITLE, + items: [ + { + key: i18n.BULK_ACTION_ADD_INVESTIGATION_FIELDS, + name: i18n.BULK_ACTION_ADD_INVESTIGATION_FIELDS, + 'data-test-subj': 'addInvestigationFieldsBulkEditRule', + onClick: handleBulkEdit(BulkActionEditTypeEnum.add_investigation_fields), + disabled: isEditDisabled, + toolTipContent: missingActionPrivileges + ? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES + : undefined, + toolTipProps: { position: 'right' }, + }, + { + key: i18n.BULK_ACTION_DELETE_INVESTIGATION_FIELDS, + name: i18n.BULK_ACTION_DELETE_INVESTIGATION_FIELDS, + 'data-test-subj': 'deleteInvestigationFieldsBulkEditRule', + onClick: handleBulkEdit(BulkActionEditTypeEnum.delete_investigation_fields), + disabled: isEditDisabled, + toolTipContent: missingActionPrivileges + ? i18n.LACK_OF_KIBANA_ACTIONS_FEATURE_PRIVILEGES + : undefined, + toolTipProps: { position: 'right' }, + }, + ], + }, ]; }, [ @@ -468,6 +512,7 @@ export const useBulkActions = ({ selectedRuleIds, hasActionsPrivileges, isAllSelected, + isBulkCustomHighlightedFieldsEnabled, loadingRuleIds, startTransaction, hasMlPermissions, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.test.ts index 0549306036fd2..29d1c03a1b0e4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.test.ts @@ -12,6 +12,9 @@ import { computeDryRunEditPayload } from './compute_dry_run_edit_payload'; describe('computeDryRunEditPayload', () => { test.each<[BulkActionEditType, unknown]>([ + [BulkActionEditTypeEnum.set_investigation_fields, { field_names: ['@timestamp'] }], + [BulkActionEditTypeEnum.delete_investigation_fields, { field_names: ['@timestamp'] }], + [BulkActionEditTypeEnum.add_investigation_fields, { field_names: ['@timestamp'] }], [BulkActionEditTypeEnum.set_index_patterns, []], [BulkActionEditTypeEnum.delete_index_patterns, []], [BulkActionEditTypeEnum.add_index_patterns, []], diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts index ba5d565e393d0..340e2345b33db 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts @@ -20,6 +20,16 @@ import { assertUnreachable } from '../../../../../../../common/utility_types'; */ export function computeDryRunEditPayload(editAction: BulkActionEditType): BulkActionEditPayload[] { switch (editAction) { + case BulkActionEditTypeEnum.add_investigation_fields: + case BulkActionEditTypeEnum.delete_investigation_fields: + case BulkActionEditTypeEnum.set_investigation_fields: + return [ + { + type: editAction, + value: { field_names: ['@timestamp'] }, + }, + ]; + case BulkActionEditTypeEnum.add_index_patterns: case BulkActionEditTypeEnum.delete_index_patterns: case BulkActionEditTypeEnum.set_index_patterns: diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 7ceb3fa661ba6..9e228cba4c74c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -159,6 +159,27 @@ export const BULK_ACTION_DELETE_TAGS = i18n.translate( } ); +export const BULK_ACTION_INVESTIGATION_FIELDS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.investigationFieldsTitle', + { + defaultMessage: 'Custom highlighted fields', + } +); + +export const BULK_ACTION_ADD_INVESTIGATION_FIELDS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.addInvestigationFieldsTitle', + { + defaultMessage: 'Add custom highlighted fields', + } +); + +export const BULK_ACTION_DELETE_INVESTIGATION_FIELDS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.deleteInvestigationFieldsTitle', + { + defaultMessage: 'Delete custom highlighted fields', + } +); + export const BULK_ACTION_APPLY_TIMELINE_TEMPLATE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.applyTimelineTemplateTitle', { @@ -408,6 +429,64 @@ export const BULK_EDIT_FLYOUT_FORM_DELETE_TAGS_TITLE = i18n.translate( } ); +export const BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_REQUIRED_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.investigationFieldsRequiredErrorMessage', + { + defaultMessage: 'A minimum of one custom highlighted field is required.', + } +); + +export const BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_OVERWRITE_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addInvestigationFieldsOverwriteCheckboxLabel', + { + defaultMessage: "Overwrite all selected rules' custom highlighted fields", + } +); + +export const BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addInvestigationFieldsComboboxLabel', + { + defaultMessage: 'Add custom highlighted fields for selected rules', + } +); + +export const BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addInvestigationFieldsComboboxHelpText', + { + defaultMessage: + 'Enter fields that you would like to add. By default, the dropdown includes fields of the index patterns defined in Security Solution advanced settings.', + } +); + +export const BULK_EDIT_FLYOUT_FORM_ADD_INVESTIGATION_FIELDS_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addInvestigationFieldsTitle', + { + defaultMessage: 'Add custom highlighted fields', + } +); + +export const BULK_EDIT_FLYOUT_FORM_DELETE_INVESTIGATION_FIELDS_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.deleteInvestigationFieldsComboboxLabel', + { + defaultMessage: 'Delete custom highlighted fields for selected rules', + } +); + +export const BULK_EDIT_FLYOUT_FORM_DELETE_INVESTIGATION_FIELDS_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.deleteInvestigationFieldsComboboxHelpText', + { + defaultMessage: + 'Enter fields that you would like to delete. By default, the dropdown includes fields of the index patterns defined in Security Solution advanced settings.', + } +); + +export const BULK_EDIT_FLYOUT_FORM_DELETE_INVESTIGATION_FIELDS_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.deleteInvestigationFieldsTitle', + { + defaultMessage: 'Delete custom highlighted fields', + } +); + export const EXPORT_FILENAME = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.exportFilenameTitle', { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx index bcae491eb2229..2084024c314e7 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.tsx @@ -278,7 +278,7 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s content={ } diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx index 08c4606b1fa6d..c4841cc35f029 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.tsx @@ -281,7 +281,7 @@ export const UserDetails: React.FC = ({ userName, timestamp, s content={ } diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.tsx index 40c3b513114e7..b06206ff5937f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.tsx @@ -71,7 +71,7 @@ export const AnalyzerPreviewContainer: React.FC = () => { tooltip: ( ), }, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx index fb0a53a5d38fb..7d4309dd6b1ad 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/session_preview_container.tsx @@ -130,7 +130,7 @@ export const SessionPreviewContainer: FC = () => { tooltip: ( ), }, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.test.tsx new file mode 100644 index 0000000000000..7f399aa095a8a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.test.tsx @@ -0,0 +1,82 @@ +/* + * 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 { createMockStore, mockGlobalState } from '../../../../common/mock'; +import { TestProviders } from '../../../../common/mock/test_providers'; + +import { TabsContent } from '.'; +import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; +import { TimelineType } from '../../../../../common/api/timeline'; +import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability'; +import { render, screen, waitFor } from '@testing-library/react'; + +jest.mock('../../../../common/hooks/esql/use_esql_availability', () => ({ + useEsqlAvailability: jest.fn().mockReturnValue({ + isESQLTabInTimelineEnabled: true, + }), +})); + +const useEsqlAvailabilityMock = useEsqlAvailability as jest.Mock; + +describe('Timeline', () => { + describe('esql tab', () => { + const esqlTabSubj = `timelineTabs-${TimelineTabs.esql}`; + const defaultProps = { + renderCellValue: () => {}, + rowRenderers: [], + timelineId: TimelineId.test, + timelineType: TimelineType.default, + timelineDescription: '', + }; + + it('should show the esql tab', () => { + render( + + + + ); + expect(screen.getByTestId(esqlTabSubj)).toBeVisible(); + }); + + it('should not show the esql tab when the advanced setting is disabled', async () => { + useEsqlAvailabilityMock.mockReturnValue({ + isESQLTabInTimelineEnabled: false, + }); + render( + + + + ); + + await waitFor(() => { + expect(screen.queryByTestId(esqlTabSubj)).toBeNull(); + }); + }); + + it('should show the esql tab when the advanced setting is disabled, but an esql query is present', async () => { + useEsqlAvailabilityMock.mockReturnValue({ + isESQLTabInTimelineEnabled: false, + }); + + const stateWithSavedSearchId = structuredClone(mockGlobalState); + stateWithSavedSearchId.timeline.timelineById[TimelineId.test].savedSearchId = 'test-id'; + const mockStore = createMockStore(stateWithSavedSearchId); + + render( + + + + ); + + await waitFor(() => { + expect(screen.queryByTestId(esqlTabSubj)).toBeVisible(); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx index 8c9d2dc1298e5..2e164677735dd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx @@ -13,8 +13,8 @@ import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo, useState import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; -import { useKibana } from '../../../../common/lib/kibana'; import { useAssistantTelemetry } from '../../../../assistant/use_assistant_telemetry'; import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability'; import type { SessionViewConfig } from '../../../../../common/types'; @@ -43,6 +43,7 @@ import * as i18n from './translations'; import { useLicense } from '../../../../common/hooks/use_license'; import { TIMELINE_CONVERSATION_TITLE } from '../../../../assistant/content/conversations/translations'; import { initializeTimelineSettings } from '../../../store/actions'; +import { selectTimelineESQLSavedSearchId } from '../../../store/selectors'; const HideShowContainer = styled.div.attrs<{ $isVisible: boolean; isOverflowYScroll: boolean }>( ({ $isVisible = false, isOverflowYScroll = false }) => ({ @@ -109,7 +110,11 @@ const ActiveTimelineTab = memo( showTimeline, }) => { const { hasAssistantPrivilege } = useAssistantAvailability(); - const isEsqlSettingEnabled = useKibana().services.configSettings.ESQLEnabled; + const { isESQLTabInTimelineEnabled } = useEsqlAvailability(); + const timelineESQLSavedSearch = useShallowEqualSelector((state) => + selectTimelineESQLSavedSearchId(state, timelineId) + ); + const shouldShowESQLTab = isESQLTabInTimelineEnabled || timelineESQLSavedSearch != null; const aiAssistantFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode'); const getTab = useCallback( (tab: TimelineTabs) => { @@ -177,7 +182,7 @@ const ActiveTimelineTab = memo( timelineId={timelineId} /> - {showTimeline && isEsqlSettingEnabled && activeTimelineTab === TimelineTabs.esql && ( + {showTimeline && shouldShowESQLTab && activeTimelineTab === TimelineTabs.esql && ( = ({ sessionViewConfig, timelineDescription, }) => { - const isEsqlTabInTimelineDisabled = useIsExperimentalFeatureEnabled('timelineEsqlTabDisabled'); const aiAssistantFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode'); - const isEsqlSettingEnabled = useKibana().services.configSettings.ESQLEnabled; const { hasAssistantPrivilege } = useAssistantAvailability(); const dispatch = useDispatch(); const getActiveTab = useMemo(() => getActiveTabSelector(), []); @@ -268,9 +271,14 @@ const TabsContentComponent: React.FC = ({ const getAppNotes = useMemo(() => getNotesSelector(), []); const getTimelineNoteIds = useMemo(() => getNoteIdsSelector(), []); const getTimelinePinnedEventNotes = useMemo(() => getEventIdToNoteIdsSelector(), []); + const { isESQLTabInTimelineEnabled } = useEsqlAvailability(); + const timelineESQLSavedSearch = useShallowEqualSelector((state) => + selectTimelineESQLSavedSearchId(state, timelineId) + ); const activeTab = useShallowEqualSelector((state) => getActiveTab(state, timelineId)); const showTimeline = useShallowEqualSelector((state) => getShowTimeline(state, timelineId)); + const shouldShowESQLTab = isESQLTabInTimelineEnabled || timelineESQLSavedSearch != null; const numberOfPinnedEvents = useShallowEqualSelector((state) => getNumberOfPinnedEvents(state, timelineId) @@ -373,7 +381,7 @@ const TabsContentComponent: React.FC = ({ {i18n.QUERY_TAB} {showTimeline && } - {!isEsqlTabInTimelineDisabled && isEsqlSettingEnabled && ( + {shouldShowESQLTab && ( time */ const selectTimelineKqlQuery = createSelector(selectTimelineById, (timeline) => timeline?.kqlQuery); +/** + * Selector that returns the timeline esql saved search id. + */ +export const selectTimelineESQLSavedSearchId = createSelector( + selectTimelineById, + (timeline) => timeline?.savedSearchId +); + /** * Selector that returns the kqlQuery.filterQuery.kuery.expression of a timeline. */ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts index 48f097ac7a860..40a3d048fb518 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts @@ -8,7 +8,6 @@ import * as z from 'zod'; import { RequiredFieldArray, - SetupGuide, RuleSignatureId, RuleVersion, BaseCreateProps, @@ -35,6 +34,5 @@ export const PrebuiltRuleAsset = BaseCreateProps.and(TypeSpecificCreateProps).an rule_id: RuleSignatureId, version: RuleVersion, required_fields: RequiredFieldArray.optional(), - setup: SetupGuide.optional(), }) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts index ed0f19c7015b6..75414a8cd08ef 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/register_routes.ts @@ -47,7 +47,7 @@ export const registerRuleManagementRoutes = ( bulkDeleteRulesRoute(router, logger); // Rules bulk actions - performBulkActionRoute(router, ml, logger); + performBulkActionRoute(router, config, ml, logger); // Rules export/import exportRulesRoute(router, config, logger); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts index 576ba1376ba3c..0b7710db26425 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.test.ts @@ -18,7 +18,12 @@ import { getFindResultWithSingleHit, getFindResultWithMultiHits, } from '../../../../routes/__mocks__/request_responses'; -import { requestContextMock, serverMock, requestMock } from '../../../../routes/__mocks__'; +import { + createMockConfig, + requestContextMock, + serverMock, + requestMock, +} from '../../../../routes/__mocks__'; import { performBulkActionRoute } from './route'; import { getPerformBulkActionEditSchemaMock, @@ -32,6 +37,7 @@ jest.mock('../../../logic/crud/read_rules', () => ({ readRules: jest.fn() })); describe('Perform bulk action route', () => { const readRulesMock = readRules as jest.Mock; + let config: ReturnType; let server: ReturnType; let { clients, context } = requestContextMock.createTools(); let ml: ReturnType; @@ -42,6 +48,7 @@ describe('Perform bulk action route', () => { server = serverMock.create(); logger = loggingSystemMock.createLogger(); ({ clients, context } = requestContextMock.createTools()); + config = createMockConfig(); ml = mlServicesMock.createSetupContract(); clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); @@ -50,7 +57,7 @@ describe('Perform bulk action route', () => { errors: [], total: 1, }); - performBulkActionRoute(server.router, ml, logger); + performBulkActionRoute(server.router, config, ml, logger); }); describe('status codes', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts index 8dc0a5ec651bb..ae0298ada97c1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts @@ -8,6 +8,7 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { AbortError } from '@kbn/kibana-utils-plugin/common'; import { transformError } from '@kbn/securitysolution-es-utils'; +import type { ConfigType } from '../../../../../../config'; import type { PerformBulkActionResponse } from '../../../../../../../common/api/detection_engine/rule_management'; import { BulkActionTypeEnum, @@ -47,6 +48,7 @@ const MAX_ROUTE_CONCURRENCY = 5; export const performBulkActionRoute = ( router: SecuritySolutionPluginRouter, + config: ConfigType, ml: SetupPlugins['ml'], logger: Logger ) => { @@ -143,6 +145,7 @@ export const performBulkActionRoute = ( ids: body.ids, actions: body.edit, mlAuthz, + experimentalFeatures: config.experimentalFeatures, }); return buildBulkResponse(response, { @@ -303,7 +306,12 @@ export const performBulkActionRoute = ( concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL, items: rules, executor: async (rule) => { - await dryRunValidateBulkEditRule({ mlAuthz, rule, edit: body.edit }); + await dryRunValidateBulkEditRule({ + mlAuthz, + rule, + edit: body.edit, + experimentalFeatures: config.experimentalFeatures, + }); return rule; }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts index fd2f1644480c0..3c7e0e7af16ee 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/bulk_edit_rules.ts @@ -7,6 +7,7 @@ import type { RulesClient } from '@kbn/alerting-plugin/server'; +import type { ExperimentalFeatures } from '../../../../../../common'; import type { BulkActionEditPayload } from '../../../../../../common/api/detection_engine/rule_management'; import type { MlAuthz } from '../../../../machine_learning/authz'; @@ -25,6 +26,7 @@ export interface BulkEditRulesArguments { filter?: string; ids?: string[]; mlAuthz: MlAuthz; + experimentalFeatures: ExperimentalFeatures; } /** @@ -40,6 +42,7 @@ export const bulkEditRules = async ({ actions, filter, mlAuthz, + experimentalFeatures, }: BulkEditRulesArguments) => { const { attributesActions, paramsActions } = splitBulkEditActions(actions); const operations = attributesActions.map(bulkEditActionToRulesClientOperation).flat(); @@ -53,7 +56,7 @@ export const bulkEditRules = async ({ edit: actions, immutable: ruleParams.immutable, }); - return ruleParamsModifier(ruleParams, paramsActions); + return ruleParamsModifier(ruleParams, paramsActions, experimentalFeatures); }, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts index 93044fc0fed18..af8fca55cf5fb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.test.ts @@ -8,6 +8,11 @@ import { addItemsToArray, deleteItemsFromArray, ruleParamsModifier } from './rule_params_modifier'; import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management'; import type { RuleAlertType } from '../../../rule_schema'; +import type { ExperimentalFeatures } from '../../../../../../common'; + +const mockExperimentalFeatures = { + bulkCustomHighlightedFieldsEnabled: true, +} as ExperimentalFeatures; describe('addItemsToArray', () => { test('should add single item to array', () => { @@ -45,22 +50,30 @@ describe('ruleParamsModifier', () => { } as RuleAlertType['params']; test('should increment version if rule is custom (immutable === false)', () => { - const { modifiedParams } = ruleParamsModifier(ruleParamsMock, [ - { - type: BulkActionEditTypeEnum.add_index_patterns, - value: ['my-index-*'], - }, - ]); + const { modifiedParams } = ruleParamsModifier( + ruleParamsMock, + [ + { + type: BulkActionEditTypeEnum.add_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ); expect(modifiedParams).toHaveProperty('version', ruleParamsMock.version + 1); }); test('should not increment version if rule is prebuilt (immutable === true)', () => { - const { modifiedParams } = ruleParamsModifier({ ...ruleParamsMock, immutable: true }, [ - { - type: BulkActionEditTypeEnum.add_index_patterns, - value: ['my-index-*'], - }, - ]); + const { modifiedParams } = ruleParamsModifier( + { ...ruleParamsMock, immutable: true }, + [ + { + type: BulkActionEditTypeEnum.add_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ); expect(modifiedParams).toHaveProperty('version', ruleParamsMock.version); }); @@ -133,7 +146,8 @@ describe('ruleParamsModifier', () => { type: BulkActionEditTypeEnum.add_index_patterns, value: indexPatternsToAdd, }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).toHaveProperty('index', resultingIndexPatterns); expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); @@ -197,7 +211,8 @@ describe('ruleParamsModifier', () => { type: BulkActionEditTypeEnum.delete_index_patterns, value: indexPatternsToDelete, }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).toHaveProperty('index', resultingIndexPatterns); expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); @@ -252,7 +267,8 @@ describe('ruleParamsModifier', () => { type: BulkActionEditTypeEnum.set_index_patterns, value: indexPatternsToOverwrite, }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).toHaveProperty('index', resultingIndexPatterns); expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); @@ -270,7 +286,8 @@ describe('ruleParamsModifier', () => { type: BulkActionEditTypeEnum.delete_index_patterns, value: ['index-2-*'], }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).not.toHaveProperty('index'); expect(isParamsUpdateSkipped).toBe(true); @@ -285,7 +302,8 @@ describe('ruleParamsModifier', () => { value: ['index'], overwrite_data_views: true, }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).toHaveProperty('dataViewId', undefined); expect(isParamsUpdateSkipped).toBe(false); @@ -300,7 +318,8 @@ describe('ruleParamsModifier', () => { value: ['index'], overwrite_data_views: true, }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).toHaveProperty('dataViewId', undefined); expect(isParamsUpdateSkipped).toBe(false); @@ -315,7 +334,8 @@ describe('ruleParamsModifier', () => { value: ['index'], overwrite_data_views: true, }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).toHaveProperty('dataViewId', undefined); expect(modifiedParams).toHaveProperty('index', ['test-*']); @@ -331,7 +351,8 @@ describe('ruleParamsModifier', () => { value: ['index'], overwrite_data_views: true, }, - ] + ], + mockExperimentalFeatures ); expect(modifiedParams).toHaveProperty('dataViewId', undefined); expect(modifiedParams).toHaveProperty('index', undefined); @@ -340,12 +361,16 @@ describe('ruleParamsModifier', () => { test('should throw error on adding index pattern if rule is of machine learning type', () => { expect(() => - ruleParamsModifier({ type: 'machine_learning' } as RuleAlertType['params'], [ - { - type: BulkActionEditTypeEnum.add_index_patterns, - value: ['my-index-*'], - }, - ]) + ruleParamsModifier( + { type: 'machine_learning' } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.add_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ) ).toThrow( "Index patterns can't be added. Machine learning rule doesn't have index patterns property" ); @@ -353,12 +378,16 @@ describe('ruleParamsModifier', () => { test('should throw error on deleting index pattern if rule is of machine learning type', () => { expect(() => - ruleParamsModifier({ type: 'machine_learning' } as RuleAlertType['params'], [ - { - type: BulkActionEditTypeEnum.delete_index_patterns, - value: ['my-index-*'], - }, - ]) + ruleParamsModifier( + { type: 'machine_learning' } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.delete_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ) ).toThrow( "Index patterns can't be deleted. Machine learning rule doesn't have index patterns property" ); @@ -366,12 +395,16 @@ describe('ruleParamsModifier', () => { test('should throw error on overwriting index pattern if rule is of machine learning type', () => { expect(() => - ruleParamsModifier({ type: 'machine_learning' } as RuleAlertType['params'], [ - { - type: BulkActionEditTypeEnum.set_index_patterns, - value: ['my-index-*'], - }, - ]) + ruleParamsModifier( + { type: 'machine_learning' } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.set_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ) ).toThrow( "Index patterns can't be overwritten. Machine learning rule doesn't have index patterns property" ); @@ -379,51 +412,404 @@ describe('ruleParamsModifier', () => { test('should throw error on adding index pattern if rule is of ES|QL type', () => { expect(() => - ruleParamsModifier({ type: 'esql' } as RuleAlertType['params'], [ - { - type: BulkActionEditTypeEnum.add_index_patterns, - value: ['my-index-*'], - }, - ]) + ruleParamsModifier( + { type: 'esql' } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.add_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ) ).toThrow("Index patterns can't be added. ES|QL rule doesn't have index patterns property"); }); test('should throw error on deleting index pattern if rule is of ES|QL type', () => { expect(() => - ruleParamsModifier({ type: 'esql' } as RuleAlertType['params'], [ - { - type: BulkActionEditTypeEnum.delete_index_patterns, - value: ['my-index-*'], - }, - ]) + ruleParamsModifier( + { type: 'esql' } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.delete_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ) ).toThrow("Index patterns can't be deleted. ES|QL rule doesn't have index patterns property"); }); test('should throw error on overwriting index pattern if rule is of ES|QL type', () => { expect(() => - ruleParamsModifier({ type: 'esql' } as RuleAlertType['params'], [ - { - type: BulkActionEditTypeEnum.set_index_patterns, - value: ['my-index-*'], - }, - ]) + ruleParamsModifier( + { type: 'esql' } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.set_index_patterns, + value: ['my-index-*'], + }, + ], + mockExperimentalFeatures + ) ).toThrow( "Index patterns can't be overwritten. ES|QL rule doesn't have index patterns property" ); }); }); + describe('investigation_fields', () => { + describe('add_investigation_fields action', () => { + test.each([ + [ + '3 existing investigation fields + 2 of them = 3 investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToAdd: { field_names: ['field-2', 'field-3'] }, + resultingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + isParamsUpdateSkipped: true, + }, + ], + [ + '3 existing investigation fields + 2 other investigation fields (none of them) = 5 investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToAdd: { field_names: ['field-4', 'field-5'] }, + resultingInvestigationFields: { + field_names: ['field-1', 'field-2', 'field-3', 'field-4', 'field-5'], + }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing investigation fields + 1 of them + 2 other investigation fields (none of them) = 5 investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToAdd: { field_names: ['field-3', 'field-4', 'field-5'] }, + resultingInvestigationFields: { + field_names: ['field-1', 'field-2', 'field-3', 'field-4', 'field-5'], + }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing investigation fields + 0 investigation fields = 3 investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToAdd: { field_names: [] }, + resultingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + isParamsUpdateSkipped: true, + }, + ], + [ + '`undefined` existing investigation fields + 1 investigation field = 1 investigation field', + { + existingInvestigationFields: undefined, + investigationFieldsToAdd: { field_names: ['field-1'] }, + resultingInvestigationFields: { field_names: ['field-1'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '`undefined` existing investigation fields + 1 investigation field = 1 investigation field', + { + existingInvestigationFields: undefined, + investigationFieldsToAdd: { field_names: ['field-1'] }, + resultingInvestigationFields: { field_names: ['field-1'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing `legacy` investigation fields + 2 other investigation fields (none of them) = 5 investigation fields', + { + existingInvestigationFields: ['field-1', 'field-2', 'field-3'], + investigationFieldsToAdd: { field_names: ['field-4', 'field-5'] }, + resultingInvestigationFields: { + field_names: ['field-1', 'field-2', 'field-3', 'field-4', 'field-5'], + }, + isParamsUpdateSkipped: false, + }, + ], + ])( + 'should add investigation fields to rule, case:"%s"', + ( + caseName, + { + existingInvestigationFields, + investigationFieldsToAdd, + resultingInvestigationFields, + isParamsUpdateSkipped, + } + ) => { + const { modifiedParams, isParamsUpdateSkipped: isUpdateSkipped } = ruleParamsModifier( + { + ...ruleParamsMock, + investigationFields: existingInvestigationFields, + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.add_investigation_fields, + value: investigationFieldsToAdd, + }, + ], + mockExperimentalFeatures + ); + expect(modifiedParams).toHaveProperty( + 'investigationFields', + resultingInvestigationFields + ); + expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); + } + ); + }); + + describe('delete_investigation_fields action', () => { + test.each([ + [ + '3 existing investigation fields - 2 of them = 1 investigation field', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToDelete: { field_names: ['field-2', 'field-3'] }, + resultingInvestigationFields: { field_names: ['field-1'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing investigation fields - 2 other investigation fields (none of them) = 3 investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToDelete: { field_names: ['field-4', 'field-5'] }, + resultingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + isParamsUpdateSkipped: true, + }, + ], + [ + '3 existing investigation fields - 1 of them - 2 other investigation fields (none of them) = 2 investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToDelete: { field_names: ['field-3', 'field-4', 'field-5'] }, + resultingInvestigationFields: { field_names: ['field-1', 'field-2'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing investigation fields - 0 investigation fields = 3 investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToDelete: { field_names: [] }, + resultingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + isParamsUpdateSkipped: true, + }, + ], + [ + '`undefined` existing investigation fields - 2 of them = `undeinfed` investigation fields', + { + existingInvestigationFields: undefined, + investigationFieldsToDelete: { field_names: ['field-2', 'field-3'] }, + resultingInvestigationFields: undefined, + isParamsUpdateSkipped: true, + }, + ], + [ + '3 existing `legacy` investigation fields - 2 of them = 1 investigation field', + { + existingInvestigationFields: ['field-1', 'field-2', 'field-3'], + investigationFieldsToDelete: { field_names: ['field-2', 'field-3'] }, + resultingInvestigationFields: { field_names: ['field-1'] }, + isParamsUpdateSkipped: false, + }, + ], + ])( + 'should delete investigation fields from rule, case:"%s"', + ( + caseName, + { + existingInvestigationFields, + investigationFieldsToDelete, + resultingInvestigationFields, + isParamsUpdateSkipped, + } + ) => { + const { modifiedParams, isParamsUpdateSkipped: isUpdateSkipped } = ruleParamsModifier( + { + ...ruleParamsMock, + investigationFields: existingInvestigationFields, + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.delete_investigation_fields, + value: investigationFieldsToDelete, + }, + ], + mockExperimentalFeatures + ); + expect(modifiedParams).toHaveProperty( + 'investigationFields', + resultingInvestigationFields + ); + expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); + } + ); + }); + + describe('set_investigation_fields action', () => { + test.each([ + [ + '3 existing investigation fields overwritten with 2 of them = 2 existing investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToOverwrite: { field_names: ['field-2', 'field-3'] }, + resultingInvestigationFields: { field_names: ['field-2', 'field-3'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing investigation fields overwritten with 2 other investigation fields = 2 other investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToOverwrite: { field_names: ['field-4', 'field-5'] }, + resultingInvestigationFields: { field_names: ['field-4', 'field-5'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing investigation fields overwritten with 1 of them + 2 other investigation fields = 1 existing investigation field + 2 other investigation fields', + { + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToOverwrite: { field_names: ['field-3', 'field-4', 'field-5'] }, + resultingInvestigationFields: { field_names: ['field-3', 'field-4', 'field-5'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '`undefined` existing investigation fields overwritten with 2 of them = 2 existing investigation fields', + { + existingInvestigationFields: undefined, + investigationFieldsToOverwrite: { field_names: ['field-2', 'field-3'] }, + resultingInvestigationFields: { field_names: ['field-2', 'field-3'] }, + isParamsUpdateSkipped: false, + }, + ], + [ + '3 existing `legacy` investigation fields overwritten with 1 of them + 2 other investigation fields = 1 existing investigation field + 2 other investigation fields', + { + existingInvestigationFields: ['field-1', 'field-2', 'field-3'], + investigationFieldsToOverwrite: { field_names: ['field-3', 'field-4', 'field-5'] }, + resultingInvestigationFields: { field_names: ['field-3', 'field-4', 'field-5'] }, + isParamsUpdateSkipped: false, + }, + ], + ])( + 'should overwrite investigation fields in rule, case:"%s"', + ( + caseName, + { + existingInvestigationFields, + investigationFieldsToOverwrite, + resultingInvestigationFields, + isParamsUpdateSkipped, + } + ) => { + const { modifiedParams, isParamsUpdateSkipped: isUpdateSkipped } = ruleParamsModifier( + { + ...ruleParamsMock, + investigationFields: existingInvestigationFields, + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.set_investigation_fields, + value: investigationFieldsToOverwrite, + }, + ], + mockExperimentalFeatures + ); + expect(modifiedParams).toHaveProperty( + 'investigationFields', + resultingInvestigationFields + ); + expect(isParamsUpdateSkipped).toBe(isUpdateSkipped); + } + ); + }); + + describe('feature flag disabled state', () => { + test('should throw error on adding investigation fields if feature is disabled', () => { + expect(() => + ruleParamsModifier( + { + ...ruleParamsMock, + investigationFields: ['field-1', 'field-2', 'field-3'], + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.add_investigation_fields, + value: { field_names: ['field-4'] }, + }, + ], + { + bulkCustomHighlightedFieldsEnabled: false, + } as ExperimentalFeatures + ) + ).toThrow("Custom highlighted fields can't be added. Feature is disabled."); + }); + + test('should throw error on overwriting investigation fields if feature is disabled', () => { + expect(() => + ruleParamsModifier( + { + ...ruleParamsMock, + investigationFields: ['field-1', 'field-2', 'field-3'], + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.set_investigation_fields, + value: { field_names: ['field-4'] }, + }, + ], + { + bulkCustomHighlightedFieldsEnabled: false, + } as ExperimentalFeatures + ) + ).toThrow("Custom highlighted fields can't be overwritten. Feature is disabled."); + }); + + test('should throw error on deleting investigation fields if feature is disabled', () => { + expect(() => + ruleParamsModifier( + { + ...ruleParamsMock, + investigationFields: ['field-1', 'field-2', 'field-3'], + } as RuleAlertType['params'], + [ + { + type: BulkActionEditTypeEnum.delete_investigation_fields, + value: { field_names: ['field-1'] }, + }, + ], + { + bulkCustomHighlightedFieldsEnabled: false, + } as ExperimentalFeatures + ) + ).toThrow("Custom highlighted fields can't be deleted. Feature is disabled."); + }); + }); + }); + describe('timeline', () => { test('should set timeline', () => { - const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier(ruleParamsMock, [ - { - type: BulkActionEditTypeEnum.set_timeline, - value: { - timeline_id: '91832785-286d-4ebe-b884-1a208d111a70', - timeline_title: 'Test timeline', + const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier( + ruleParamsMock, + [ + { + type: BulkActionEditTypeEnum.set_timeline, + value: { + timeline_id: '91832785-286d-4ebe-b884-1a208d111a70', + timeline_title: 'Test timeline', + }, }, - }, - ]); + ], + mockExperimentalFeatures + ); expect(modifiedParams.timelineId).toBe('91832785-286d-4ebe-b884-1a208d111a70'); expect(modifiedParams.timelineTitle).toBe('Test timeline'); @@ -436,15 +822,19 @@ describe('ruleParamsModifier', () => { const INTERVAL_IN_MINUTES = 5; const LOOKBACK_IN_MINUTES = 1; const FROM_IN_SECONDS = (INTERVAL_IN_MINUTES + LOOKBACK_IN_MINUTES) * 60; - const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier(ruleParamsMock, [ - { - type: BulkActionEditTypeEnum.set_schedule, - value: { - interval: `${INTERVAL_IN_MINUTES}m`, - lookback: `${LOOKBACK_IN_MINUTES}m`, + const { modifiedParams, isParamsUpdateSkipped } = ruleParamsModifier( + ruleParamsMock, + [ + { + type: BulkActionEditTypeEnum.set_schedule, + value: { + interval: `${INTERVAL_IN_MINUTES}m`, + lookback: `${LOOKBACK_IN_MINUTES}m`, + }, }, - }, - ]); + ], + mockExperimentalFeatures + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((modifiedParams as any).interval).toBeUndefined(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts index 6bfdfcf394aac..2cae6218ab76c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/rule_params_modifier.ts @@ -8,10 +8,12 @@ import moment from 'moment'; import { parseInterval } from '@kbn/data-plugin/common/search/aggs/utils/date_interval_utils'; import type { RuleParamsModifierResult } from '@kbn/alerting-plugin/server/rules_client/methods/bulk_edit'; -import type { RuleAlertType } from '../../../rule_schema'; +import type { ExperimentalFeatures } from '../../../../../../common'; +import type { InvestigationFieldsCombined, RuleAlertType } from '../../../rule_schema'; import type { BulkActionEditForRuleParams, BulkActionEditPayloadIndexPatterns, + BulkActionEditPayloadInvestigationFields, } from '../../../../../../common/api/detection_engine/rule_management'; import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management'; import { invariant } from '../../../../../../common/utils/invariant'; @@ -63,9 +65,52 @@ const shouldSkipIndexPatternsBulkAction = ( return false; }; +// Check if the investigation fields added to the rule already exist in it +const hasInvestigationFields = ( + investigationFields: InvestigationFieldsCombined | undefined, + action: BulkActionEditPayloadInvestigationFields +) => + action.value.field_names.every((field) => + (Array.isArray(investigationFields) + ? investigationFields + : investigationFields?.field_names ?? [] + ).includes(field) + ); + +// Check if the investigation fields to be deleted don't exist in the rule +const hasNoInvestigationFields = ( + investigationFields: InvestigationFieldsCombined | undefined, + action: BulkActionEditPayloadInvestigationFields +) => + action.value.field_names.every( + (field) => + !( + Array.isArray(investigationFields) + ? investigationFields + : investigationFields?.field_names ?? [] + ).includes(field) + ); + +const shouldSkipInvestigationFieldsBulkAction = ( + investigationFields: InvestigationFieldsCombined | undefined, + action: BulkActionEditPayloadInvestigationFields +) => { + if (action.type === BulkActionEditTypeEnum.add_investigation_fields) { + return hasInvestigationFields(investigationFields, action); + } + + if (action.type === BulkActionEditTypeEnum.delete_investigation_fields) { + return hasNoInvestigationFields(investigationFields, action); + } + + return false; +}; + +// eslint-disable-next-line complexity const applyBulkActionEditToRuleParams = ( existingRuleParams: RuleAlertType['params'], - action: BulkActionEditForRuleParams + action: BulkActionEditForRuleParams, + experimentalFeatures: ExperimentalFeatures ): { ruleParams: RuleAlertType['params']; isActionSkipped: boolean; @@ -151,6 +196,69 @@ const applyBulkActionEditToRuleParams = ( ruleParams.index = action.value; break; } + // investigation_fields actions + case BulkActionEditTypeEnum.add_investigation_fields: { + invariant( + experimentalFeatures.bulkCustomHighlightedFieldsEnabled, + "Custom highlighted fields can't be added. Feature is disabled." + ); + + if (shouldSkipInvestigationFieldsBulkAction(ruleParams.investigationFields, action)) { + isActionSkipped = true; + break; + } + + ruleParams.investigationFields = { + field_names: addItemsToArray( + (Array.isArray(ruleParams.investigationFields) + ? ruleParams.investigationFields + : ruleParams.investigationFields?.field_names) ?? [], + action.value.field_names + ), + }; + break; + } + case BulkActionEditTypeEnum.delete_investigation_fields: { + invariant( + experimentalFeatures.bulkCustomHighlightedFieldsEnabled, + "Custom highlighted fields can't be deleted. Feature is disabled." + ); + + if (shouldSkipInvestigationFieldsBulkAction(ruleParams.investigationFields, action)) { + isActionSkipped = true; + break; + } + + if (ruleParams.investigationFields) { + const fieldNames = deleteItemsFromArray( + (Array.isArray(ruleParams.investigationFields) + ? ruleParams.investigationFields + : ruleParams.investigationFields?.field_names) ?? [], + action.value.field_names + ); + ruleParams.investigationFields = + fieldNames.length > 0 + ? { + field_names: fieldNames, + } + : undefined; + } + break; + } + case BulkActionEditTypeEnum.set_investigation_fields: { + invariant( + experimentalFeatures.bulkCustomHighlightedFieldsEnabled, + "Custom highlighted fields can't be overwritten. Feature is disabled." + ); + + if (shouldSkipInvestigationFieldsBulkAction(ruleParams.investigationFields, action)) { + isActionSkipped = true; + break; + } + + ruleParams.investigationFields = action.value; + break; + } // timeline actions case BulkActionEditTypeEnum.set_timeline: { ruleParams = { @@ -192,12 +300,17 @@ const applyBulkActionEditToRuleParams = ( */ export const ruleParamsModifier = ( existingRuleParams: RuleAlertType['params'], - actions: BulkActionEditForRuleParams[] + actions: BulkActionEditForRuleParams[], + experimentalFeatures: ExperimentalFeatures ): RuleParamsModifierResult => { let isParamsUpdateSkipped = true; const modifiedParams = actions.reduce((acc, action) => { - const { ruleParams, isActionSkipped } = applyBulkActionEditToRuleParams(acc, action); + const { ruleParams, isActionSkipped } = applyBulkActionEditToRuleParams( + acc, + action, + experimentalFeatures + ); // The rule was updated with at least one action, so mark our rule as updated if (!isActionSkipped) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.ts index 18634fb7162b7..08b3487ede61f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/utils.ts @@ -21,3 +21,18 @@ export const isIndexPatternsBulkEditAction = (editAction: BulkActionEditType) => ]; return indexPatternsActions.includes(editAction); }; + +/** + * helper utility that defines whether bulk edit action is related to investigation fields, i.e. one of: + * 'add_investigation_fields', 'delete_investigation_fields', 'set_investigation_fields' + * @param editAction {@link BulkActionEditType} + * @returns {boolean} + */ +export const isInvestigationFieldsBulkEditAction = (editAction: BulkActionEditType) => { + const investigationFieldsActions: BulkActionEditType[] = [ + BulkActionEditTypeEnum.add_investigation_fields, + BulkActionEditTypeEnum.delete_investigation_fields, + BulkActionEditTypeEnum.set_investigation_fields, + ]; + return investigationFieldsActions.includes(editAction); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts index 806c90e41ac12..092808cf1cc5c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/bulk_actions/validations.ts @@ -6,6 +6,7 @@ */ import type { Type as RuleType } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { ExperimentalFeatures } from '../../../../../../common'; import { invariant } from '../../../../../../common/utils/invariant'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { isEsqlRule } from '../../../../../../common/detection_engine/utils'; @@ -16,7 +17,7 @@ import type { } from '../../../../../../common/api/detection_engine/rule_management'; import { BulkActionEditTypeEnum } from '../../../../../../common/api/detection_engine/rule_management'; import type { RuleAlertType } from '../../../rule_schema'; -import { isIndexPatternsBulkEditAction } from './utils'; +import { isIndexPatternsBulkEditAction, isInvestigationFieldsBulkEditAction } from './utils'; import { throwDryRunError } from './dry_run'; import type { MlAuthz } from '../../../../machine_learning/authz'; import { throwAuthzError } from '../../../../machine_learning/validation'; @@ -37,6 +38,7 @@ interface DryRunBulkEditBulkActionsValidationArgs { rule: RuleAlertType; mlAuthz: MlAuthz; edit: BulkActionEditPayload[]; + experimentalFeatures: ExperimentalFeatures; } /** @@ -113,6 +115,7 @@ export const dryRunValidateBulkEditRule = async ({ rule, edit, mlAuthz, + experimentalFeatures, }: DryRunBulkEditBulkActionsValidationArgs) => { await validateBulkEditRule({ ruleType: rule.params.type, @@ -142,4 +145,15 @@ export const dryRunValidateBulkEditRule = async ({ ), BulkActionsDryRunErrCode.ESQL_INDEX_PATTERN ); + + // check whether "custom highlighted fields" feature is enabled + await throwDryRunError( + () => + invariant( + experimentalFeatures.bulkCustomHighlightedFieldsEnabled || + !edit.some((action) => isInvestigationFieldsBulkEditAction(action.type)), + 'Bulk custom highlighted fields action feature is disabled.' + ), + BulkActionsDryRunErrCode.INVESTIGATION_FIELDS_FEATURE + ); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts index 5cbaa44834302..8c4137bf3d386 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts @@ -431,7 +431,7 @@ export const previewRulesRoute = ( ); break; case 'esql': - if (!config.settings.ESQLEnabled || config.experimentalFeatures.esqlRulesDisabled) { + if (config.experimentalFeatures.esqlRulesDisabled) { throw Error('ES|QL rule type is not supported'); } const esqlAlertType = previewRuleTypeWrapper(createEsqlAlertType(ruleOptions)); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index b0eb25bd3c18f..e97abbb1f0474 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -320,7 +320,7 @@ export class Plugin implements ISecuritySolutionPlugin { const securityRuleTypeWrapper = createSecurityRuleTypeWrapper(securityRuleTypeOptions); plugins.alerting.registerType(securityRuleTypeWrapper(createEqlAlertType(ruleOptions))); - if (config.settings.ESQLEnabled && !experimentalFeatures.esqlRulesDisabled) { + if (!experimentalFeatures.esqlRulesDisabled) { plugins.alerting.registerType(securityRuleTypeWrapper(createEsqlAlertType(ruleOptions))); } plugins.alerting.registerType( diff --git a/x-pack/plugins/task_manager/server/metrics/create_aggregator.test.ts b/x-pack/plugins/task_manager/server/metrics/create_aggregator.test.ts index d5d51671fafc6..9edc6ef1b61b7 100644 --- a/x-pack/plugins/task_manager/server/metrics/create_aggregator.test.ts +++ b/x-pack/plugins/task_manager/server/metrics/create_aggregator.test.ts @@ -565,6 +565,194 @@ describe('createAggregator', () => { clock.restore(); }); }); + + test('does not reset count when configured metrics reset interval expires if metrics have been reset via reset$ event', async () => { + const reset$ = new Subject(); + const clock = sinon.useFakeTimers(); + clock.tick(0); + const events1 = [ + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + taskClaimFailureEvent, + taskClaimSuccessEvent, + ]; + + const events2 = [ + taskClaimSuccessEvent, + taskClaimFailureEvent, + taskClaimFailureEvent, + taskClaimSuccessEvent, + taskClaimSuccessEvent, + ]; + const events$ = new Subject(); + + const taskClaimAggregator = createAggregator({ + key: 'task_claim', + events$, + config: { + ...config, + metrics_reset_interval: 50, + }, + reset$, + eventFilter: (event: TaskLifecycleEvent) => isTaskPollingCycleEvent(event), + metricsAggregator: new TaskClaimMetricsAggregator(), + }); + + return new Promise((resolve) => { + taskClaimAggregator + .pipe( + // skip initial metric which is just initialized data which + // ensures we don't stall on combineLatest + skip(1), + take(events1.length + events2.length + 1), + bufferCount(events1.length + events2.length + 1) + ) + .subscribe((metrics: Array>) => { + expect(metrics[0]).toEqual({ + key: 'task_claim', + value: { + success: 1, + total: 1, + total_errors: 0, + duration: { counts: [1], values: [100] }, + duration_values: [10], + }, + }); + expect(metrics[1]).toEqual({ + key: 'task_claim', + value: { + success: 2, + total: 2, + total_errors: 0, + duration: { counts: [2], values: [100] }, + duration_values: [10, 10], + }, + }); + expect(metrics[2]).toEqual({ + key: 'task_claim', + value: { + success: 3, + total: 3, + total_errors: 0, + duration: { counts: [3], values: [100] }, + duration_values: [10, 10, 10], + }, + }); + expect(metrics[3]).toEqual({ + key: 'task_claim', + value: { + success: 4, + total: 4, + total_errors: 0, + duration: { counts: [4], values: [100] }, + duration_values: [10, 10, 10, 10], + }, + }); + expect(metrics[4]).toEqual({ + key: 'task_claim', + value: { + success: 4, + total: 5, + total_errors: 1, + duration: { counts: [4], values: [100] }, + duration_values: [10, 10, 10, 10], + }, + }); + expect(metrics[5]).toEqual({ + key: 'task_claim', + value: { + success: 5, + total: 6, + total_errors: 1, + duration: { counts: [5], values: [100] }, + duration_values: [10, 10, 10, 10, 10], + }, + }); + // reset interval fired here but stats should not clear + expect(metrics[6]).toEqual({ + key: 'task_claim', + value: { + success: 6, + total: 7, + total_errors: 1, + duration: { counts: [6], values: [100] }, + duration_values: [10, 10, 10, 10, 10, 10], + }, + }); + expect(metrics[7]).toEqual({ + key: 'task_claim', + value: { + success: 6, + total: 8, + total_errors: 2, + duration: { counts: [6], values: [100] }, + duration_values: [10, 10, 10, 10, 10, 10], + }, + }); + expect(metrics[8]).toEqual({ + key: 'task_claim', + value: { + success: 6, + total: 9, + total_errors: 3, + duration: { counts: [6], values: [100] }, + duration_values: [10, 10, 10, 10, 10, 10], + }, + }); + expect(metrics[9]).toEqual({ + key: 'task_claim', + value: { + success: 7, + total: 10, + total_errors: 3, + duration: { counts: [7], values: [100] }, + duration_values: [10, 10, 10, 10, 10, 10, 10], + }, + }); + expect(metrics[10]).toEqual({ + key: 'task_claim', + value: { + success: 8, + total: 11, + total_errors: 3, + duration: { counts: [8], values: [100] }, + duration_values: [10, 10, 10, 10, 10, 10, 10, 10], + }, + }); + // reset interval fired here and stats should have cleared + expect(metrics[11]).toEqual({ + key: 'task_claim', + value: { + success: 1, + total: 1, + total_errors: 0, + duration: { counts: [1], values: [100] }, + duration_values: [10], + }, + }); + resolve(); + }); + + // reset$ event at 10 seconds + clock.tick(10); + reset$.next(true); + for (const event of events1) { + events$.next(event); + } + // metrics reset event but counts should not reset + clock.tick(40); + for (const event of events2) { + events$.next(event); + } + // metric reset event should clear + clock.tick(50); + events$.next(taskClaimSuccessEvent); + + clock.restore(); + }); + }); }); describe('with TaskRunMetricsAggregator', () => { diff --git a/x-pack/plugins/task_manager/server/metrics/create_aggregator.ts b/x-pack/plugins/task_manager/server/metrics/create_aggregator.ts index 32d625524ead6..c1c4c6517e1ea 100644 --- a/x-pack/plugins/task_manager/server/metrics/create_aggregator.ts +++ b/x-pack/plugins/task_manager/server/metrics/create_aggregator.ts @@ -30,13 +30,31 @@ export function createAggregator({ metricsAggregator, }: CreateMetricsAggregatorOpts): AggregatedStatProvider { if (reset$) { + let lastResetTime: Date = new Date(); // Resets the aggregators either when the reset interval has passed or // a reset$ event is received merge( - interval(config.metrics_reset_interval).pipe(map(() => true)), - reset$.pipe(map(() => true)) - ).subscribe(() => { - metricsAggregator.reset(); + interval(config.metrics_reset_interval).pipe( + map(() => { + if (intervalHasPassedSince(lastResetTime, config.metrics_reset_interval)) { + lastResetTime = new Date(); + return true; + } + + return false; + }) + ), + reset$.pipe( + map((value: boolean) => { + // keep track of the last time we reset due to collection + lastResetTime = new Date(); + return true; + }) + ) + ).subscribe((shouldReset: boolean) => { + if (shouldReset) { + metricsAggregator.reset(); + } }); } @@ -57,3 +75,8 @@ export function createAggregator({ }) ); } + +function intervalHasPassedSince(date: Date, intervalInMs: number) { + const now = new Date().valueOf(); + return now - date.valueOf() > intervalInMs; +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts index 92e43bbd83dc9..541f83fc8d412 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/migrations.ts @@ -15,8 +15,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - // FLAKY: https://github.com/elastic/kibana/issues/169159 - describe.skip('migrations', () => { + describe('migrations', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/actions'); }); diff --git a/x-pack/test/api_integration/apis/ml/indices/field_caps.ts b/x-pack/test/api_integration/apis/ml/indices/field_caps.ts index c979600a16138..0baa7e39969c4 100644 --- a/x-pack/test/api_integration/apis/ml/indices/field_caps.ts +++ b/x-pack/test/api_integration/apis/ml/indices/field_caps.ts @@ -30,7 +30,8 @@ export default ({ getService }: FtrProviderContext) => { return body; } - describe('field_caps', () => { + // Failing ES Promotion: https://github.com/elastic/kibana/issues/182514 + describe.skip('field_caps', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); }); diff --git a/x-pack/test/functional/es_archives/actions/data.json b/x-pack/test/functional/es_archives/actions/data.json index 75206672358db..f164af6a33f16 100644 --- a/x-pack/test/functional/es_archives/actions/data.json +++ b/x-pack/test/functional/es_archives/actions/data.json @@ -1,7 +1,7 @@ { "type": "doc", "value": { - "index": ".kibana", + "index": ".kibana_alerting_cases", "id": "action:uuid-actionId", "source": { "type": "action", @@ -18,7 +18,7 @@ "type": "doc", "value": { "id": "action:791a2ab1-784a-46ea-aa68-04c837e5da2d", - "index": ".kibana_1", + "index": ".kibana_alerting_cases", "source": { "action": { "actionTypeId": ".jira", @@ -61,7 +61,7 @@ "type": "doc", "value": { "id": "action:949f909b-20a0-46e3-aadb-6a4d117bb592", - "index": ".kibana_1", + "index": ".kibana_alerting_cases", "source": { "action": { "actionTypeId": ".webhook", @@ -73,11 +73,9 @@ "name": "A webhook with auth", "secrets": "LUqlrITACjqPmcWGlbl+H4RsGGOlw8LM0Urq8r7y6jNT7Igv3J7FjKJ2NXfNTaghVBO7e9x3wZOtiycwyoAdviTyYm1pspni24vH+OT70xaSuXcDoxfGwiLEcaG04INDnUJX4dtmRerxqR9ChktC70LNtOU3sqjYI2tWt2vOqGeq" }, - "migrationVersion": { - "action": "7.10.0" - }, "references": [ ], + "typeMigrationVersion": "7.10.0", "type": "action", "updated_at": "2020-10-26T21:29:47.380Z" } @@ -88,7 +86,7 @@ "type": "doc", "value": { "id": "action:7434121e-045a-47d6-a0a6-0b6da752397a", - "index": ".kibana_1", + "index": ".kibana_alerting_cases", "source": { "action": { "actionTypeId": ".webhook", @@ -100,11 +98,9 @@ "name": "A webhook with no auth", "secrets": "tOwFq20hbUrcp3FX7stKB5aJaQQdLNQwomSNym8BgnFaBBafPOASv5T0tGdGsTr/CA7VK+N/wYBHQPzt0apF8Z/UYl63ZXqck5tSoFDnQW77zv1VVQ5wEwN1qkAQQcfrXTXU2wYVAYZNSuHkbeRjcasfG0ty1K+J7A==" }, - "migrationVersion": { - "action": "7.10.0" - }, "references": [ ], + "typeMigrationVersion": "7.10.0", "type": "action", "updated_at": "2020-10-26T21:30:35.146Z" } @@ -115,7 +111,7 @@ "type": "doc", "value": { "id": "action:0f8f2810-0a59-11ec-9a7c-fd0c2b83ff7c", - "index": ".kibana_1", + "index": ".kibana_alerting_cases", "source": { "action": { "actionTypeId" : ".email", @@ -131,9 +127,7 @@ }, "secrets" : "V2EJEtTv3yTFi1kdglhNahnKYWCS+J7aWCJQU+eEqGPZEz6n7G1NsBWoh7IY0FteLTilTteQXyY/Eg3k/7bb0G8Mz+WBZ1mRvUggGTFqgoOptyUsvHoBhv0R/1bCTCabN3Pe88AfnC+VDXqwuMifpmgKEEsKF3H8VONv7TYO02FW" }, - "migrationVersion": { - "action": "7.14.0" - }, + "typeMigrationVersion": "7.14.0", "coreMigrationVersion" : "7.15.0", "references": [ ], @@ -147,7 +141,7 @@ "type": "doc", "value": { "id": "action:1e0824a0-0a59-11ec-9a7c-fd0c2b83ff7c", - "index": ".kibana_1", + "index": ".kibana_alerting_cases", "source": { "action": { "actionTypeId" : ".email", @@ -163,9 +157,7 @@ }, "secrets" : "iw/bRTXZQXOV0ODocb6FQnHR6AyeVyD91We03llNStyTNFwuHVWdFl6ZdiEEeDOadBMeJomvp/dAfQevGpbwWdclcu9F87x3CfeGqV9DtBy0dXRbx9PzKBwgJdK3ucHQDFAs8ZXQbefvCOFjCHGAsJDPhTKj5rTUyg==" }, - "migrationVersion": { - "action": "7.14.0" - }, + "typeMigrationVersion": "7.14.0", "coreMigrationVersion" : "7.15.0", "references": [ ], @@ -179,7 +171,7 @@ "type": "doc", "value": { "id": "action:0f8f2810-0a59-11ec-9a7c-fd0c2b83ff7d", - "index": ".kibana_1", + "index": ".kibana_alerting_cases", "source": { "action": { "actionTypeId" : ".email", @@ -195,9 +187,7 @@ }, "secrets" : "V2EJEtTv3yTFi1kdglhNahasdfnKYWCS+J7aWCJQU+eEqGPZEz6n7G1NsBWoh7IY0FteLTilTteQXyY/Eg3k/7bb0G8Mz+WBZ1mRvUggGTFqgoOptyUsvHoBhv0R/1bCTCabN3Pe88AfnC+VDXqwuMifpmgKEEsKF3H8VONv7TYO02FW" }, - "migrationVersion": { - "action": "7.14.0" - }, + "typeMigrationVersion": "7.14.0", "coreMigrationVersion" : "7.15.0", "references": [ ], @@ -211,7 +201,7 @@ "type": "doc", "value": { "id": "action:7d04bc30-c4c0-11ec-ae29-917aa31a5b75", - "index": ".kibana_1", + "index": ".kibana_alerting_cases", "source": { "action": { "actionTypeId" : ".servicenow-sir", @@ -223,9 +213,7 @@ }, "secrets" : "kPp4tl4ueQ2ZNWSfATR3dFrbxd+NNBo4MY8izS6GJf358Lmeg/YaYjb2rIymrbPktR6HnPBRaVyXWlRTvBGstRicJc0LJHZbx3wNJlTRIj4UFlVqZLGQWQ/GcSqFLSZ1JQbKwgAvyfLtF6BhjAhGYEovK3/OLUNzGc3gvUOOHBiPWjiAY8A=" }, - "migrationVersion": { - "action": "8.0.0" - }, + "typeMigrationVersion": "8.0.0", "coreMigrationVersion" : "8.2.0", "references": [ ], @@ -242,7 +230,7 @@ "type": "doc", "value": { "id": "action:8a9331b0-c4c0-11ec-ae29-917aa31a5b75", - "index": ".kibana_1", + "index": ".kibana_alerting_cases", "source": { "action": { "actionTypeId" : ".servicenow-itom", @@ -253,9 +241,7 @@ }, "secrets" : "yYThM4vbrSTIg5IjKWE+eMDrxzL7UO0JQIyh6FvEMgqoNREUxRrIavSo25v+DXQIX1DyfsvjjKg97pNPlZhvS3siCwDZZafSFrwkCKDl+S4KHORgIMX+slilcQeuEnzwit7bFxcY7Y/AcNF8Ks6jO0Gs1UR58ibSPUALXoK2VOlJnHSgtvE=" }, - "migrationVersion": { - "action": "8.0.0" - }, + "typeMigrationVersion": "8.0.0", "coreMigrationVersion" : "8.2.0", "references": [ ], @@ -272,7 +258,7 @@ "type": "doc", "value": { "id": "action:6d3a1250-c4c0-11ec-ae29-917aa31a5b75", - "index": ".kibana_1", + "index": ".kibana_alerting_cases", "source": { "action": { "actionTypeId" : ".servicenow", @@ -284,9 +270,7 @@ }, "secrets" : "zfXUDtG0CyJkJUKnQ8rSqo75hb6ZhbRUWkV1NiFEjApM87b72Rcqz3Fv+sbm8eBDOO1Fdd9CVyK+Bfly4ZwVCgL2lR0qIbPzz34q36r267dnGVsaERyJIVv2WPy+EGdiRZKgfpy4XFbMNT1R3gyIsUkd4TT+McqGfVTont2XTFIpMW2A9y8=" }, - "migrationVersion": { - "action": "8.0.0" - }, + "typeMigrationVersion": "8.0.0", "coreMigrationVersion" : "8.2.0", "references": [ ], diff --git a/x-pack/test/functional/es_archives/actions/mappings.json b/x-pack/test/functional/es_archives/actions/mappings.json deleted file mode 100644 index a0da38c85f724..0000000000000 --- a/x-pack/test/functional/es_archives/actions/mappings.json +++ /dev/null @@ -1,2525 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "6e96ac5e648f57523879661ea72525b7", - "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "7b44fba6773e37c806ce290ea9b7024e", - "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", - "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", - "cases": "32aa96a6d3855ddda53010ae2048ac22", - "cases-comments": "c2061fb929f585df57425102fa928b4b", - "cases-configure": "42711cbb311976c0687853f4c1354572", - "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", - "config": "c63748b75f39d0c54de12d12c1ccbc20", - "dashboard": "d00f614b29a80360e1190193fd333bab", - "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", - "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862", - "epm-packages": "8f6e0b09ea0374c4ffe98c3755373cff", - "exception-list": "497afa2f881a675d72d58e20057f3d8b", - "exception-list-agnostic": "497afa2f881a675d72d58e20057f3d8b", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", - "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", - "fleet-agents": "034346488514b7058a79140b19ddf631", - "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", - "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", - "index-pattern": "66eccb05066c5a89924f48a9e9736499", - "infrastructure-ui-source": "2b2809653635caf490c93f090502d04c", - "ingest-agent-policies": "9326f99c977fd2ef5ab24b6336a0675c", - "ingest-outputs": "8aa988c376e65443fefc26f1075e93a3", - "ingest-package-policies": "8545e51d7bc8286d6dace3d41240d749", - "ingest_manager_settings": "012cf278ec84579495110bb827d1ed09", - "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "d33c68a69ff1e78c9888dedd2164ac22", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "4a05b35c3a3a58fbc72dd0202dc3487f", - "maps-telemetry": "5ef305b18111b77789afefbd36b66171", - "metrics-explorer-view": "a8df1d270ee48c969d22d23812d08187", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "namespace": "2f4316de49999235636386fe51dc06c1", - "namespaces": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "5c4b9a6effceb17ae8a0ab22d0c49767", - "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", - "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", - "siem-ui-timeline": "94bc38c7a421d15fbfe8ea565370a421", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "telemetry": "36a616f7026dfa617d6655df850fe16d", - "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "52d7a13ad68a150c4525b292d23e12cc", - "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" - } - }, - "dynamic": "strict", - "properties": { - "action": { - "properties": { - "actionTypeId": { - "type": "keyword" - }, - "isMissingSecrets": { - "type": "boolean" - }, - "config": { - "enabled": false, - "type": "object" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "secrets": { - "type": "binary" - } - } - }, - "action_task_params": { - "properties": { - "actionId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "params": { - "enabled": false, - "type": "object" - } - } - }, - "alert": { - "properties": { - "actions": { - "properties": { - "actionRef": { - "type": "keyword" - }, - "actionTypeId": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - }, - "type": "nested" - }, - "alertTypeId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "apiKeyOwner": { - "type": "keyword" - }, - "consumer": { - "type": "keyword" - }, - "createdAt": { - "type": "date" - }, - "createdBy": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "muteAll": { - "type": "boolean" - }, - "mutedInstanceIds": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "params": { - "enabled": false, - "type": "object" - }, - "schedule": { - "properties": { - "interval": { - "type": "keyword" - } - } - }, - "scheduledTaskId": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "throttle": { - "type": "keyword" - }, - "updatedBy": { - "type": "keyword" - } - } - }, - "apm-indices": { - "properties": { - "error": { - "type": "keyword" - }, - "metric": { - "type": "keyword" - }, - "onboarding": { - "type": "keyword" - }, - "sourcemap": { - "type": "keyword" - }, - "span": { - "type": "keyword" - }, - "transaction": { - "type": "keyword" - } - } - }, - "apm-telemetry": { - "dynamic": "false", - "type": "object" - }, - "app_search_telemetry": { - "dynamic": "false", - "type": "object" - }, - "application_usage_daily": { - "dynamic": "false", - "properties": { - "timestamp": { - "type": "date" - } - } - }, - "application_usage_totals": { - "dynamic": "false", - "type": "object" - }, - "application_usage_transactional": { - "dynamic": "false", - "type": "object" - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "content": { - "type": "text" - }, - "help": { - "type": "text" - }, - "image": { - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad-template": { - "dynamic": "false", - "properties": { - "help": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "tags": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "template_key": { - "type": "keyword" - } - } - }, - "cases": { - "properties": { - "closed_at": { - "type": "date" - }, - "closed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "connector_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "description": { - "type": "text" - }, - "external_service": { - "properties": { - "connector_id": { - "type": "keyword" - }, - "connector_name": { - "type": "keyword" - }, - "external_id": { - "type": "keyword" - }, - "external_title": { - "type": "text" - }, - "external_url": { - "type": "text" - }, - "pushed_at": { - "type": "date" - }, - "pushed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "status": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-comments": { - "properties": { - "comment": { - "type": "text" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "pushed_at": { - "type": "date" - }, - "pushed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-configure": { - "properties": { - "closure_type": { - "type": "keyword" - }, - "connector_id": { - "type": "keyword" - }, - "connector_name": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-user-actions": { - "properties": { - "action": { - "type": "keyword" - }, - "action_at": { - "type": "date" - }, - "action_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "action_field": { - "type": "keyword" - }, - "new_value": { - "type": "text" - }, - "old_value": { - "type": "text" - } - } - }, - "config": { - "dynamic": "false", - "properties": { - "buildNum": { - "type": "keyword" - } - } - }, - "coreMigrationVersion": { - "type": "keyword" - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "endpoint:user-artifact": { - "properties": { - "body": { - "type": "binary" - }, - "compressionAlgorithm": { - "index": false, - "type": "keyword" - }, - "created": { - "index": false, - "type": "date" - }, - "decodedSha256": { - "index": false, - "type": "keyword" - }, - "decodedSize": { - "index": false, - "type": "long" - }, - "encodedSha256": { - "type": "keyword" - }, - "encodedSize": { - "index": false, - "type": "long" - }, - "encryptionAlgorithm": { - "index": false, - "type": "keyword" - }, - "identifier": { - "type": "keyword" - } - } - }, - "endpoint:user-artifact-manifest": { - "properties": { - "created": { - "index": false, - "type": "date" - }, - "schemaVersion": { - "type": "keyword" - }, - "semanticVersion": { - "index": false, - "type": "keyword" - }, - "artifacts": { - "type": "nested", - "properties": { - "policyId": { - "type": "keyword", - "index": false - }, - "artifactId": { - "type": "keyword", - "index": false - } - } - } - } - }, - "epm-packages": { - "properties": { - "es_index_patterns": { - "enabled": false, - "type": "object" - }, - "installed_es": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "installed_kibana": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "internal": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "removable": { - "type": "boolean" - }, - "version": { - "type": "keyword" - } - } - }, - "exception-list": { - "properties": { - "_tags": { - "type": "keyword" - }, - "comments": { - "properties": { - "comment": { - "type": "keyword" - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "updated_at": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "description": { - "type": "keyword" - }, - "entries": { - "properties": { - "entries": { - "properties": { - "field": { - "type": "keyword" - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "field": { - "type": "keyword" - }, - "list": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "immutable": { - "type": "boolean" - }, - "item_id": { - "type": "keyword" - }, - "list_id": { - "type": "keyword" - }, - "list_type": { - "type": "keyword" - }, - "meta": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "tie_breaker_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "exception-list-agnostic": { - "properties": { - "_tags": { - "type": "keyword" - }, - "comments": { - "properties": { - "comment": { - "type": "keyword" - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "updated_at": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "description": { - "type": "keyword" - }, - "entries": { - "properties": { - "entries": { - "properties": { - "field": { - "type": "keyword" - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "field": { - "type": "keyword" - }, - "list": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "immutable": { - "type": "boolean" - }, - "item_id": { - "type": "keyword" - }, - "list_id": { - "type": "keyword" - }, - "list_type": { - "type": "keyword" - }, - "meta": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "tie_breaker_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, - "fleet-agent-actions": { - "properties": { - "agent_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "data": { - "type": "binary" - }, - "sent_at": { - "type": "date" - }, - "type": { - "type": "keyword" - } - } - }, - "fleet-agent-events": { - "properties": { - "action_id": { - "type": "keyword" - }, - "agent_id": { - "type": "keyword" - }, - "config_id": { - "type": "keyword" - }, - "data": { - "type": "text" - }, - "message": { - "type": "text" - }, - "payload": { - "type": "text" - }, - "stream_id": { - "type": "keyword" - }, - "subtype": { - "type": "keyword" - }, - "timestamp": { - "type": "date" - }, - "type": { - "type": "keyword" - } - } - }, - "fleet-agents": { - "properties": { - "access_api_key_id": { - "type": "keyword" - }, - "active": { - "type": "boolean" - }, - "config_id": { - "type": "keyword" - }, - "config_revision": { - "type": "integer" - }, - "current_error_events": { - "index": false, - "type": "text" - }, - "default_api_key": { - "type": "binary" - }, - "default_api_key_id": { - "type": "keyword" - }, - "enrolled_at": { - "type": "date" - }, - "last_checkin": { - "type": "date" - }, - "last_checkin_status": { - "type": "keyword" - }, - "last_updated": { - "type": "date" - }, - "local_metadata": { - "type": "flattened" - }, - "packages": { - "type": "keyword" - }, - "shared_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "unenrolled_at": { - "type": "date" - }, - "unenrollment_started_at": { - "type": "date" - }, - "updated_at": { - "type": "date" - }, - "user_provided_metadata": { - "type": "flattened" - }, - "version": { - "type": "keyword" - } - } - }, - "fleet-enrollment-api-keys": { - "properties": { - "active": { - "type": "boolean" - }, - "api_key": { - "type": "binary" - }, - "api_key_id": { - "type": "keyword" - }, - "config_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "expire_at": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "infrastructure-ui-source": { - "properties": { - "description": { - "type": "text" - }, - "fields": { - "properties": { - "container": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "pod": { - "type": "keyword" - }, - "tiebreaker": { - "type": "keyword" - }, - "timestamp": { - "type": "keyword" - } - } - }, - "inventoryDefaultView": { - "type": "keyword" - }, - "logAlias": { - "type": "keyword" - }, - "logColumns": { - "properties": { - "fieldColumn": { - "properties": { - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } - } - }, - "messageColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "timestampColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - } - }, - "type": "nested" - }, - "metricAlias": { - "type": "keyword" - }, - "metricsExplorerDefaultView": { - "type": "keyword" - }, - "name": { - "type": "text" - } - } - }, - "ingest-agent-policies": { - "properties": { - "description": { - "type": "text" - }, - "is_default": { - "type": "boolean" - }, - "monitoring_enabled": { - "index": false, - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "namespace": { - "type": "keyword" - }, - "package_configs": { - "type": "keyword" - }, - "revision": { - "type": "integer" - }, - "status": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "ingest-outputs": { - "properties": { - "ca_sha256": { - "index": false, - "type": "keyword" - }, - "config": { - "type": "flattened" - }, - "fleet_enroll_password": { - "type": "binary" - }, - "fleet_enroll_username": { - "type": "binary" - }, - "hosts": { - "type": "keyword" - }, - "is_default": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "ingest-package-policies": { - "properties": { - "config_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "enabled": { - "type": "boolean" - }, - "inputs": { - "enabled": false, - "properties": { - "config": { - "type": "flattened" - }, - "enabled": { - "type": "boolean" - }, - "streams": { - "properties": { - "compiled_stream": { - "type": "flattened" - }, - "config": { - "type": "flattened" - }, - "data_stream": { - "properties": { - "dataset": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "enabled": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "vars": { - "type": "flattened" - } - }, - "type": "nested" - }, - "type": { - "type": "keyword" - }, - "vars": { - "type": "flattened" - } - }, - "type": "nested" - }, - "name": { - "type": "keyword" - }, - "namespace": { - "type": "keyword" - }, - "output_id": { - "type": "keyword" - }, - "package": { - "properties": { - "name": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "revision": { - "type": "integer" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "ingest_manager_settings": { - "properties": { - "agent_auto_upgrade": { - "type": "keyword" - }, - "has_seen_add_data_notice": { - "index": false, - "type": "boolean" - }, - "kibana_ca_sha256": { - "type": "keyword" - }, - "kibana_url": { - "type": "keyword" - }, - "package_auto_upgrade": { - "type": "keyword" - } - } - }, - "inventory-view": { - "properties": { - "accountId": { - "type": "keyword" - }, - "autoBounds": { - "type": "boolean" - }, - "autoReload": { - "type": "boolean" - }, - "boundsOverride": { - "properties": { - "max": { - "type": "integer" - }, - "min": { - "type": "integer" - } - } - }, - "customMetrics": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "label": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "customOptions": { - "properties": { - "field": { - "type": "keyword" - }, - "text": { - "type": "keyword" - } - }, - "type": "nested" - }, - "filterQuery": { - "properties": { - "expression": { - "type": "keyword" - }, - "kind": { - "type": "keyword" - } - } - }, - "groupBy": { - "properties": { - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - }, - "type": "nested" - }, - "legend": { - "properties": { - "palette": { - "type": "keyword" - }, - "reverseColors": { - "type": "boolean" - }, - "steps": { - "type": "long" - } - } - }, - "metric": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "label": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "nodeType": { - "type": "keyword" - }, - "region": { - "type": "keyword" - }, - "sort": { - "properties": { - "by": { - "type": "keyword" - }, - "direction": { - "type": "keyword" - } - } - }, - "time": { - "type": "long" - }, - "view": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "lens": { - "properties": { - "description": { - "type": "text" - }, - "expression": { - "index": false, - "type": "keyword" - }, - "state": { - "type": "flattened" - }, - "title": { - "type": "text" - }, - "visualizationType": { - "type": "keyword" - } - } - }, - "lens-ui-telemetry": { - "properties": { - "count": { - "type": "integer" - }, - "date": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "map": { - "properties": { - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "enabled": false, - "type": "object" - }, - "metrics-explorer-view": { - "properties": { - "chartOptions": { - "properties": { - "stack": { - "type": "boolean" - }, - "type": { - "type": "keyword" - }, - "yAxisMode": { - "type": "keyword" - } - } - }, - "currentTimerange": { - "properties": { - "from": { - "type": "keyword" - }, - "interval": { - "type": "keyword" - }, - "to": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "options": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "filterQuery": { - "type": "keyword" - }, - "forceInterval": { - "type": "boolean" - }, - "groupBy": { - "type": "keyword" - }, - "limit": { - "type": "integer" - }, - "metrics": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "color": { - "type": "keyword" - }, - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - }, - "type": "nested" - }, - "source": { - "type": "keyword" - } - } - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "config": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "namespaces": { - "type": "keyword" - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "index": false, - "type": "keyword" - } - } - }, - "timefilter": { - "enabled": false, - "type": "object" - }, - "title": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "index": false, - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "index": false, - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "index": false, - "type": "text" - } - } - }, - "sort": { - "index": false, - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "search-telemetry": { - "dynamic": "false", - "type": "object" - }, - "siem-detection-engine-rule-actions": { - "properties": { - "actions": { - "properties": { - "action_type_id": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - } - }, - "alertThrottle": { - "type": "keyword" - }, - "ruleAlertId": { - "type": "keyword" - }, - "ruleThrottle": { - "type": "keyword" - } - } - }, - "siem-detection-engine-rule-status": { - "properties": { - "alertId": { - "type": "keyword" - }, - "bulkCreateTimeDurations": { - "type": "float" - }, - "gap": { - "type": "text" - }, - "lastFailureAt": { - "type": "date" - }, - "lastFailureMessage": { - "type": "text" - }, - "lastLookBackDate": { - "type": "date" - }, - "lastSuccessAt": { - "type": "date" - }, - "lastSuccessMessage": { - "type": "text" - }, - "searchAfterTimeDurations": { - "type": "float" - }, - "status": { - "type": "keyword" - }, - "statusDate": { - "type": "date" - } - } - }, - "siem-ui-timeline": { - "properties": { - "columns": { - "properties": { - "aggregatable": { - "type": "boolean" - }, - "category": { - "type": "keyword" - }, - "columnHeaderType": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "example": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "indexes": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "placeholder": { - "type": "text" - }, - "searchable": { - "type": "boolean" - }, - "type": { - "type": "keyword" - } - } - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "dataProviders": { - "properties": { - "and": { - "properties": { - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - }, - "type": { - "type": "text" - } - } - }, - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - }, - "type": { - "type": "text" - } - } - }, - "dateRange": { - "properties": { - "end": { - "type": "date" - }, - "start": { - "type": "date" - } - } - }, - "description": { - "type": "text" - }, - "eventType": { - "type": "keyword" - }, - "excludedRowRendererIds": { - "type": "text" - }, - "favorite": { - "properties": { - "favoriteDate": { - "type": "date" - }, - "fullName": { - "type": "text" - }, - "keySearch": { - "type": "text" - }, - "userName": { - "type": "text" - } - } - }, - "filters": { - "properties": { - "exists": { - "type": "text" - }, - "match_all": { - "type": "text" - }, - "meta": { - "properties": { - "alias": { - "type": "text" - }, - "controlledBy": { - "type": "text" - }, - "disabled": { - "type": "boolean" - }, - "field": { - "type": "text" - }, - "formattedValue": { - "type": "text" - }, - "index": { - "type": "keyword" - }, - "key": { - "type": "keyword" - }, - "negate": { - "type": "boolean" - }, - "params": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "text" - } - } - }, - "missing": { - "type": "text" - }, - "query": { - "type": "text" - }, - "range": { - "type": "text" - }, - "script": { - "type": "text" - } - } - }, - "kqlMode": { - "type": "keyword" - }, - "kqlQuery": { - "properties": { - "filterQuery": { - "properties": { - "kuery": { - "properties": { - "expression": { - "type": "text" - }, - "kind": { - "type": "keyword" - } - } - }, - "serializedQuery": { - "type": "text" - } - } - } - } - }, - "savedQueryId": { - "type": "keyword" - }, - "sort": { - "properties": { - "columnId": { - "type": "keyword" - }, - "sortDirection": { - "type": "keyword" - } - } - }, - "status": { - "type": "keyword" - }, - "templateTimelineId": { - "type": "text" - }, - "templateTimelineVersion": { - "type": "integer" - }, - "timelineType": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-note": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "note": { - "type": "text" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-pinned-event": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "imageUrl": { - "index": false, - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "allowChangingOptInStatus": { - "type": "boolean" - }, - "enabled": { - "type": "boolean" - }, - "lastReported": { - "type": "date" - }, - "lastVersionChecked": { - "type": "keyword" - }, - "reportFailureCount": { - "type": "integer" - }, - "reportFailureVersion": { - "type": "keyword" - }, - "sendUsageFrom": { - "type": "keyword" - }, - "userHasSeenNotice": { - "type": "boolean" - } - } - }, - "tsvb-validation-telemetry": { - "properties": { - "failedRequests": { - "type": "long" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-reindex-operation": { - "properties": { - "errorMessage": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "indexName": { - "type": "keyword" - }, - "lastCompletedStep": { - "type": "long" - }, - "locked": { - "type": "date" - }, - "newIndexName": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "reindexOptions": { - "properties": { - "openAndClose": { - "type": "boolean" - }, - "queueSettings": { - "properties": { - "queuedAt": { - "type": "long" - }, - "startedAt": { - "type": "long" - } - } - } - } - }, - "reindexTaskId": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "reindexTaskPercComplete": { - "type": "float" - }, - "runningReindexCount": { - "type": "integer" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "null_value": true, - "type": "boolean" - } - } - } - } - }, - "ui_open": { - "properties": { - "cluster": { - "null_value": 0, - "type": "long" - }, - "indices": { - "null_value": 0, - "type": "long" - }, - "overview": { - "null_value": 0, - "type": "long" - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "null_value": 0, - "type": "long" - }, - "open": { - "null_value": 0, - "type": "long" - }, - "start": { - "null_value": 0, - "type": "long" - }, - "stop": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "uptime-dynamic-settings": { - "properties": { - "certAgeThreshold": { - "type": "long" - }, - "certExpirationThreshold": { - "type": "long" - }, - "heartbeatIndices": { - "type": "keyword" - } - } - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchRefName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - }, - "workplace_search_telemetry": { - "dynamic": "false", - "type": "object" - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} diff --git a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts index c2fe5a723627f..cc47b97377e6c 100644 --- a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts @@ -82,6 +82,7 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s 'previewTelemetryUrlEnabled', 'riskScoringPersistence', 'riskScoringRoutesEnabled', + 'bulkCustomHighlightedFieldsEnabled', ])}`, '--xpack.task_manager.poll_interval=1000', `--xpack.actions.preconfigured=${JSON.stringify(PRECONFIGURED_ACTION_CONNECTORS)}`, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts index 137ee1f67b9b3..f6ba7fa49895e 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts @@ -17,5 +17,8 @@ export default createTestConfig({ 'testing_ignored.constant', '/testing_regex*/', ])}`, // See tests within the file "ignore_fields.ts" which use these values in "alertIgnoreFields" + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'bulkCustomHighlightedFieldsEnabled', + ])}`, ], }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts index fb7543b9fe700..d077ca34d782f 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action.ts @@ -88,7 +88,8 @@ export default ({ getService }: FtrProviderContext): void => { const createWebHookConnector = () => createConnector(getWebHookAction()); const createSlackConnector = () => createConnector(getSlackAction()); - describe('@ess @serverless @skipInServerless perform_bulk_action', () => { + // Failing: See https://github.com/elastic/kibana/issues/182431 + describe.skip('@ess @serverless @skipInServerless perform_bulk_action', () => { beforeEach(async () => { await createAlertsIndex(supertest, log); await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); @@ -140,6 +141,7 @@ export default ({ getService }: FtrProviderContext): void => { { package: 'package-a', version: '^1.2.3' }, { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, ], + setup: '# some setup markdown', }; const mockRule = getCustomQueryRuleParams(defaultableFields); @@ -314,6 +316,7 @@ export default ({ getService }: FtrProviderContext): void => { const ruleId = 'ruleId'; const ruleToDuplicate = getCustomQueryRuleParams({ rule_id: ruleId, + setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, @@ -1151,6 +1154,210 @@ export default ({ getService }: FtrProviderContext): void => { ); }); + describe('investigation fields actions', () => { + it('should set investigation fields in rules', async () => { + const ruleId = 'ruleId'; + await createRule(supertest, log, getSimpleRule(ruleId)); + + const { body: bulkEditResponse } = await securitySolutionApi + .performBulkAction({ + query: {}, + body: { + query: '', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.set_investigation_fields, + value: { field_names: ['field-1'] }, + }, + ], + }, + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + + // Check that the updated rule is returned with the response + expect(bulkEditResponse.attributes.results.updated[0].investigation_fields).toEqual({ + field_names: ['field-1'], + }); + + // Check that the updates have been persisted + const { body: updatedRule } = await fetchRule(ruleId).expect(200); + + expect(updatedRule.investigation_fields).toEqual({ field_names: ['field-1'] }); + }); + + it('should add investigation fields to rules', async () => { + const ruleId = 'ruleId'; + const investigationFields = { field_names: ['field-1', 'field-2'] }; + const resultingFields = { field_names: ['field-1', 'field-2', 'field-3'] }; + await createRule(supertest, log, { + ...getSimpleRule(ruleId), + investigation_fields: investigationFields, + }); + + const { body: bulkEditResponse } = await securitySolutionApi + .performBulkAction({ + query: {}, + body: { + query: '', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.add_investigation_fields, + value: { field_names: ['field-3'] }, + }, + ], + }, + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + + // Check that the updated rule is returned with the response + expect(bulkEditResponse.attributes.results.updated[0].investigation_fields).toEqual( + resultingFields + ); + + // Check that the updates have been persisted + const { body: updatedRule } = await fetchRule(ruleId).expect(200); + + expect(updatedRule.investigation_fields).toEqual(resultingFields); + }); + + it('should delete investigation fields from rules', async () => { + const ruleId = 'ruleId'; + const investigationFields = { field_names: ['field-1', 'field-2'] }; + const resultingFields = { field_names: ['field-1'] }; + await createRule(supertest, log, { + ...getSimpleRule(ruleId), + investigation_fields: investigationFields, + }); + + const { body: bulkEditResponse } = await securitySolutionApi + .performBulkAction({ + query: {}, + body: { + query: '', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: BulkActionEditTypeEnum.delete_investigation_fields, + value: { field_names: ['field-2'] }, + }, + ], + }, + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).toEqual({ + failed: 0, + skipped: 0, + succeeded: 1, + total: 1, + }); + + // Check that the updated rule is returned with the response + expect(bulkEditResponse.attributes.results.updated[0].investigation_fields).toEqual( + resultingFields + ); + + // Check that the updates have been persisted + const { body: updatedRule } = await fetchRule(ruleId).expect(200); + + expect(updatedRule.investigation_fields).toEqual(resultingFields); + }); + + const skipIndexPatternsUpdateCases = [ + // Delete no-ops + { + caseName: '0 existing fields - 2 fields = 0 fields', + existingInvestigationFields: undefined, + investigationFieldsToUpdate: { field_names: ['field-1', 'field-2'] }, + resultingInvestigationFields: undefined, + operation: BulkActionEditTypeEnum.delete_investigation_fields, + }, + { + caseName: '3 existing fields - 2 other fields (none of them) = 3 fields', + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToUpdate: { field_names: ['field-8', 'field-9'] }, + resultingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + operation: BulkActionEditTypeEnum.delete_investigation_fields, + }, + // Add no-ops + { + caseName: '3 existing fields + 2 exisiting fields= 3 fields', + existingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + investigationFieldsToUpdate: { field_names: ['field-1', 'field-2'] }, + resultingInvestigationFields: { field_names: ['field-1', 'field-2', 'field-3'] }, + operation: BulkActionEditTypeEnum.add_investigation_fields, + }, + ]; + + skipIndexPatternsUpdateCases.forEach( + ({ + caseName, + existingInvestigationFields, + investigationFieldsToUpdate, + resultingInvestigationFields, + operation, + }) => { + it(`should skip rule updated for investigation fields, case: "${caseName}"`, async () => { + const ruleId = 'ruleId'; + + await createRule(supertest, log, { + ...getSimpleRule(ruleId), + investigation_fields: existingInvestigationFields, + }); + + const { body: bulkEditResponse } = await securitySolutionApi + .performBulkAction({ + query: {}, + body: { + query: '', + action: BulkActionTypeEnum.edit, + [BulkActionTypeEnum.edit]: [ + { + type: operation, + value: investigationFieldsToUpdate, + }, + ], + }, + }) + .expect(200); + + expect(bulkEditResponse.attributes.summary).toEqual({ + failed: 0, + skipped: 1, + succeeded: 0, + total: 1, + }); + + // Check that the rules is returned as skipped with expected skip reason + expect(bulkEditResponse.attributes.results.skipped[0].skip_reason).toEqual( + 'RULE_NOT_MODIFIED' + ); + + // Check that the no changes have been persisted + const { body: updatedRule } = await fetchRule(ruleId).expect(200); + + expect(updatedRule.investigation_fields).toEqual(resultingInvestigationFields); + }); + } + ); + }); + it('should set timeline template values in rule', async () => { const ruleId = 'ruleId'; const timelineId = '91832785-286d-4ebe-b884-1a208d111a70'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts index a70df8eb58335..7295ebe9b769d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts @@ -46,7 +46,8 @@ export default ({ getService }: FtrProviderContext): void => { const createWebHookConnector = () => createConnector(getWebHookAction()); // Failing: See https://github.com/elastic/kibana/issues/173804 - describe('@ess perform_bulk_action - ESS specific logic', () => { + // Failing: See https://github.com/elastic/kibana/issues/182512 + describe.skip('@ess perform_bulk_action - ESS specific logic', () => { beforeEach(async () => { await deleteAllRules(supertest, log); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules.ts index 6ba0cc273c8c5..f0f6eae7b5da0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules.ts @@ -72,6 +72,7 @@ export default ({ getService }: FtrProviderContext) => { it('should create a rule with defaultable fields', async () => { const expectedRule = getCustomQueryRuleParams({ rule_id: 'rule-1', + setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules_bulk.ts index 17b4ea3e3604e..052841b442f9b 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/basic_license_essentials_tier/create_rules_bulk.ts @@ -10,9 +10,9 @@ import expect from 'expect'; import { EsArchivePathBuilder } from '../../../../../es_archive_path_builder'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { + getCustomQueryRuleParams, getSimpleRule, getSimpleRuleOutput, - getCustomQueryRuleParams, getSimpleRuleOutputWithoutRuleId, getSimpleRuleWithoutRuleId, removeServerGeneratedProperties, @@ -71,6 +71,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should create a rule with defaultable fields', async () => { const expectedRule = getCustomQueryRuleParams({ rule_id: 'rule-1', + setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/trial_license_complete_tier/create_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/trial_license_complete_tier/create_rules.ts index 319a4a20c9c96..e6e4e4697d099 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/trial_license_complete_tier/create_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_creation/trial_license_complete_tier/create_rules.ts @@ -691,27 +691,5 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); - - describe('setup guide', async () => { - beforeEach(async () => { - await deleteAllAlerts(supertest, log, es); - await deleteAllRules(supertest, log); - }); - - it('creates a rule with a setup guide when setup parameter is present', async () => { - const { body } = await supertest - .post(DETECTION_ENGINE_RULES_URL) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .send( - getCustomQueryRuleParams({ - setup: 'A setup guide', - }) - ) - .expect(200); - - expect(body.setup).toEqual('A setup guide'); - }); - }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts index d91c1ab18b44a..c217846af4612 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/export_rules.ts @@ -7,41 +7,28 @@ import expect from 'expect'; -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; import { BaseDefaultableFields } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { binaryToString, getCustomQueryRuleParams } from '../../../utils'; -import { - createRule, - createAlertsIndex, - deleteAllRules, - deleteAllAlerts, -} from '../../../../../../common/utils/security_solution'; +import { deleteAllRules } from '../../../../../../common/utils/security_solution'; export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const securitySolutionApi = getService('securitySolutionApi'); const log = getService('log'); - const es = getService('es'); + const securitySolutionApi = getService('securitySolutionApi'); describe('@ess @serverless export_rules', () => { describe('exporting rules', () => { - beforeEach(async () => { - await createAlertsIndex(supertest, log); - }); - afterEach(async () => { - await deleteAllAlerts(supertest, log, es); await deleteAllRules(supertest, log); }); it('should set the response content types to be expected', async () => { - await createRule(supertest, log, getCustomQueryRuleParams()); + const ruleToExport = getCustomQueryRuleParams(); - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_export`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .send() + await securitySolutionApi.createRule({ body: ruleToExport }); + + await securitySolutionApi + .exportRules({ query: {}, body: null }) .expect(200) .expect('Content-Type', 'application/ndjson') .expect('Content-Disposition', 'attachment; filename="export.ndjson"'); @@ -50,13 +37,10 @@ export default ({ getService }: FtrProviderContext): void => { it('should export a single rule with a rule_id', async () => { const ruleToExport = getCustomQueryRuleParams(); - await createRule(supertest, log, ruleToExport); + await securitySolutionApi.createRule({ body: ruleToExport }); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_export`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .send() + const { body } = await securitySolutionApi + .exportRules({ query: {}, body: null }) .expect(200) .parse(binaryToString); @@ -71,6 +55,7 @@ export default ({ getService }: FtrProviderContext): void => { { package: 'package-a', version: '^1.2.3' }, { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, ], + setup: '# some setup markdown', }; const ruleToExport = getCustomQueryRuleParams(defaultableFields); @@ -87,13 +72,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should have export summary reflecting a number of rules', async () => { - await createRule(supertest, log, getCustomQueryRuleParams()); + const ruleToExport = getCustomQueryRuleParams(); + + await securitySolutionApi.createRule({ body: ruleToExport }); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_export`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .send() + const { body } = await securitySolutionApi + .exportRules({ query: {}, body: null }) .expect(200) .parse(binaryToString); @@ -111,14 +95,11 @@ export default ({ getService }: FtrProviderContext): void => { const ruleToExport1 = getCustomQueryRuleParams({ rule_id: 'rule-1' }); const ruleToExport2 = getCustomQueryRuleParams({ rule_id: 'rule-2' }); - await createRule(supertest, log, ruleToExport1); - await createRule(supertest, log, ruleToExport2); + await securitySolutionApi.createRule({ body: ruleToExport1 }); + await securitySolutionApi.createRule({ body: ruleToExport2 }); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_export`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .send() + const { body } = await securitySolutionApi + .exportRules({ query: {}, body: null }) .expect(200) .parse(binaryToString); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts index f4fc373965df9..c0bf497fbd0ca 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/basic_license_essentials_tier/import_rules.ts @@ -7,51 +7,35 @@ import expect from 'expect'; -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; import { BaseDefaultableFields } from '@kbn/security-solution-plugin/common/api/detection_engine'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; -import { getCustomQueryRuleParams, combineToNdJson, fetchRule } from '../../../utils'; -import { - createAlertsIndex, - deleteAllRules, - deleteAllAlerts, - createRule, -} from '../../../../../../common/utils/security_solution'; +import { getCustomQueryRuleParams, combineToNdJson } from '../../../utils'; +import { deleteAllRules } from '../../../../../../common/utils/security_solution'; export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const securitySolutionApi = getService('securitySolutionApi'); const log = getService('log'); - const es = getService('es'); describe('@ess @serverless import_rules', () => { describe('importing rules with an index', () => { - beforeEach(async () => { - await createAlertsIndex(supertest, log); - }); - afterEach(async () => { - await deleteAllAlerts(supertest, log, es); await deleteAllRules(supertest, log); }); it('should set the response content types to be expected', async () => { const ndjson = combineToNdJson(getCustomQueryRuleParams()); - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + await securitySolutionApi + .importRules({ query: {} }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); }); it('should reject with an error if the file type is not that of a ndjson', async () => { - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + const { body } = await securitySolutionApi + .importRules({ query: {} }) .attach('file', Buffer.from(''), 'rules.txt') .expect(400); @@ -64,10 +48,8 @@ export default ({ getService }: FtrProviderContext): void => { it('should report that it imported a simple rule successfully', async () => { const ndjson = combineToNdJson(getCustomQueryRuleParams()); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + const { body } = await securitySolutionApi + .importRules({ query: {} }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); @@ -83,14 +65,16 @@ export default ({ getService }: FtrProviderContext): void => { const ruleToImport = getCustomQueryRuleParams({ rule_id: 'rule-to-import' }); const ndjson = combineToNdJson(ruleToImport); - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + await securitySolutionApi + .importRules({ query: {} }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - const importedRule = await fetchRule(supertest, { ruleId: 'rule-to-import' }); + const { body: importedRule } = await securitySolutionApi + .readRule({ + query: { rule_id: 'rule-to-import' }, + }) + .expect(200); expect(importedRule).toMatchObject(ruleToImport); }); @@ -103,10 +87,8 @@ export default ({ getService }: FtrProviderContext): void => { }) ); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + const { body } = await securitySolutionApi + .importRules({ query: {} }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); @@ -125,10 +107,8 @@ export default ({ getService }: FtrProviderContext): void => { }) ); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + const { body } = await securitySolutionApi + .importRules({ query: {} }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); @@ -139,6 +119,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should be able to import rules with defaultable fields', async () => { const defaultableFields: BaseDefaultableFields = { + setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, @@ -174,10 +155,8 @@ export default ({ getService }: FtrProviderContext): void => { }) ); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + const { body } = await securitySolutionApi + .importRules({ query: {} }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); @@ -201,10 +180,8 @@ export default ({ getService }: FtrProviderContext): void => { }) ) ); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + const { body } = await securitySolutionApi + .importRules({ query: {} }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); @@ -242,10 +219,8 @@ export default ({ getService }: FtrProviderContext): void => { ) ); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + const { body } = await securitySolutionApi + .importRules({ query: {} }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(500); @@ -265,10 +240,8 @@ export default ({ getService }: FtrProviderContext): void => { }) ); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + const { body } = await securitySolutionApi + .importRules({ query: {} }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); @@ -298,10 +271,8 @@ export default ({ getService }: FtrProviderContext): void => { }) ); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + const { body } = await securitySolutionApi + .importRules({ query: { overwrite: true } }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); @@ -318,14 +289,12 @@ export default ({ getService }: FtrProviderContext): void => { rule_id: 'rule-1', }); - await createRule(supertest, log, ruleToImport); + await securitySolutionApi.createRule({ body: ruleToImport }); const ndjson = combineToNdJson(ruleToImport); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + const { body } = await securitySolutionApi + .importRules({ query: {} }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); @@ -350,14 +319,12 @@ export default ({ getService }: FtrProviderContext): void => { rule_id: 'rule-1', }); - await createRule(supertest, log, ruleToImport); + await securitySolutionApi.createRule({ body: ruleToImport }); const ndjson = combineToNdJson(ruleToImport); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + const { body } = await securitySolutionApi + .importRules({ query: { overwrite: true } }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); @@ -370,13 +337,11 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should overwrite an existing rule if overwrite is set to true', async () => { - await createRule( - supertest, - log, - getCustomQueryRuleParams({ - rule_id: 'rule-to-overwrite', - }) - ); + const ruleToImport = getCustomQueryRuleParams({ + rule_id: 'rule-to-overwrite', + }); + + await securitySolutionApi.createRule({ body: ruleToImport }); const ndjson = combineToNdJson( getCustomQueryRuleParams({ @@ -385,14 +350,16 @@ export default ({ getService }: FtrProviderContext): void => { }) ); - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + await securitySolutionApi + .importRules({ query: { overwrite: true } }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - const importedRule = await fetchRule(supertest, { ruleId: 'rule-to-overwrite' }); + const { body: importedRule } = await securitySolutionApi + .readRule({ + query: { rule_id: 'rule-to-overwrite' }, + }) + .expect(200); expect(importedRule).toMatchObject({ name: 'some other name', @@ -400,15 +367,17 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should bump a revision when overwriting a rule', async () => { - await createRule( - supertest, - log, - getCustomQueryRuleParams({ - rule_id: 'rule-to-overwrite', - }) - ); + const ruleToImport = getCustomQueryRuleParams({ + rule_id: 'rule-to-overwrite', + }); - const ruleBeforeOverwriting = await fetchRule(supertest, { ruleId: 'rule-to-overwrite' }); + await securitySolutionApi.createRule({ body: ruleToImport }); + + const { body: ruleBeforeOverwriting } = await securitySolutionApi + .readRule({ + query: { rule_id: 'rule-to-overwrite' }, + }) + .expect(200); const ndjson = combineToNdJson( getCustomQueryRuleParams({ @@ -417,14 +386,16 @@ export default ({ getService }: FtrProviderContext): void => { }) ); - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite=true`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + await securitySolutionApi + .importRules({ query: { overwrite: true } }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - const ruleAfterOverwriting = await fetchRule(supertest, { ruleId: 'rule-to-overwrite' }); + const { body: ruleAfterOverwriting } = await securitySolutionApi + .readRule({ + query: { rule_id: 'rule-to-overwrite' }, + }) + .expect(200); expect(ruleBeforeOverwriting).toMatchObject({ revision: 0, @@ -435,13 +406,11 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should report a conflict if there is an attempt to import a rule with a rule_id that already exists, but still have some successes with other rules', async () => { - await createRule( - supertest, - log, - getCustomQueryRuleParams({ - rule_id: 'existing-rule', - }) - ); + const ruleToImport = getCustomQueryRuleParams({ + rule_id: 'existing-rule', + }); + + await securitySolutionApi.createRule({ body: ruleToImport }); const ndjson = combineToNdJson( getCustomQueryRuleParams({ @@ -455,10 +424,8 @@ export default ({ getService }: FtrProviderContext): void => { }) ); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + const { body } = await securitySolutionApi + .importRules({ query: {} }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); @@ -479,20 +446,16 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should report a mix of conflicts and a mix of successes', async () => { - await createRule( - supertest, - log, - getCustomQueryRuleParams({ + await securitySolutionApi.createRule({ + body: getCustomQueryRuleParams({ rule_id: 'existing-rule-1', - }) - ); - await createRule( - supertest, - log, - getCustomQueryRuleParams({ + }), + }); + await securitySolutionApi.createRule({ + body: getCustomQueryRuleParams({ rule_id: 'existing-rule-2', - }) - ); + }), + }); const ndjson = combineToNdJson( getCustomQueryRuleParams({ @@ -506,10 +469,8 @@ export default ({ getService }: FtrProviderContext): void => { }) ); - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + const { body } = await securitySolutionApi + .importRules({ query: {} }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); @@ -547,21 +508,33 @@ export default ({ getService }: FtrProviderContext): void => { rule_id: 'non-existing-rule', }); - await createRule(supertest, log, existingRule1); - await createRule(supertest, log, existingRule2); + await securitySolutionApi.createRule({ body: existingRule1 }); + await securitySolutionApi.createRule({ body: existingRule2 }); const ndjson = combineToNdJson(existingRule1, existingRule2, ruleToImportSuccessfully); - await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_import`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') + await securitySolutionApi + .importRules({ query: {} }) .attach('file', Buffer.from(ndjson), 'rules.ndjson') .expect(200); - const rule1 = await fetchRule(supertest, { ruleId: 'existing-rule-1' }); - const rule2 = await fetchRule(supertest, { ruleId: 'existing-rule-2' }); - const rule3 = await fetchRule(supertest, { ruleId: 'non-existing-rule' }); + const { body: rule1 } = await securitySolutionApi + .readRule({ + query: { rule_id: 'existing-rule-1' }, + }) + .expect(200); + + const { body: rule2 } = await securitySolutionApi + .readRule({ + query: { rule_id: 'existing-rule-2' }, + }) + .expect(200); + + const { body: rule3 } = await securitySolutionApi + .readRule({ + query: { rule_id: 'non-existing-rule' }, + }) + .expect(200); expect(rule1).toMatchObject(existingRule1); expect(rule2).toMatchObject(existingRule2); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts index 27990708215d3..2dc21264ef66c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules.ts @@ -63,6 +63,7 @@ export default ({ getService }: FtrProviderContext) => { it('should patch defaultable fields', async () => { const expectedRule = getCustomQueryRuleParams({ rule_id: 'rule-1', + setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, @@ -77,6 +78,7 @@ export default ({ getService }: FtrProviderContext) => { .patchRule({ body: { rule_id: 'rule-1', + setup: expectedRule.setup, related_integrations: expectedRule.related_integrations, }, }) diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts index ef3c944bf9931..020d9c0e62b3f 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/basic_license_essentials_tier/patch_rules_bulk.ts @@ -62,6 +62,7 @@ export default ({ getService }: FtrProviderContext) => { it('should patch defaultable fields', async () => { const expectedRule = getCustomQueryRuleParams({ rule_id: 'rule-1', + setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, @@ -77,6 +78,7 @@ export default ({ getService }: FtrProviderContext) => { body: [ { rule_id: 'rule-1', + setup: expectedRule.setup, related_integrations: expectedRule.related_integrations, }, ], diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules.ts index 8256b7734463f..4a69f208c3bd5 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_patch/trial_license_complete_tier/patch_rules.ts @@ -656,37 +656,5 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); - - describe('setup guide', () => { - beforeEach(async () => { - await createAlertsIndex(supertest, log); - }); - - afterEach(async () => { - await deleteAllAlerts(supertest, log, es); - await deleteAllRules(supertest, log); - }); - - it('should overwrite setup field on patch', async () => { - await createRule(supertest, log, { - ...getSimpleRule('rule-1'), - setup: 'A setup guide', - }); - - const rulePatch = { - rule_id: 'rule-1', - setup: 'A different setup guide', - }; - - const { body } = await supertest - .patch(DETECTION_ENGINE_RULES_URL) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '2023-10-31') - .send(rulePatch) - .expect(200); - - expect(body.setup).to.eql('A different setup guide'); - }); - }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts index 08dfdac9a7e82..abd486b1e080e 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules.ts @@ -68,6 +68,7 @@ export default ({ getService }: FtrProviderContext) => { it('should update a rule with defaultable fields', async () => { const expectedRule = getCustomQueryRuleParams({ rule_id: 'rule-1', + setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts index d28d9efd41350..b73b8c0be95cc 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/basic_license_essentials_tier/update_rules_bulk.ts @@ -67,6 +67,7 @@ export default ({ getService }: FtrProviderContext) => { it('should update a rule with defaultable fields', async () => { const expectedRule = getCustomQueryRuleParams({ rule_id: 'rule-1', + setup: '# some setup markdown', related_integrations: [ { package: 'package-a', version: '^1.2.3' }, { package: 'package-b', integration: 'integration-b', version: '~1.1.1' }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/trial_license_complete_tier/update_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/trial_license_complete_tier/update_rules.ts index d905c57aa4a2f..6d120a7944759 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/trial_license_complete_tier/update_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_update/trial_license_complete_tier/update_rules.ts @@ -757,40 +757,6 @@ export default ({ getService }: FtrProviderContext) => { expect(body.investigation_fields).to.eql(undefined); }); }); - - describe('setup guide', () => { - it('should overwrite setup value on update', async () => { - await createRule(supertest, log, { - ...getSimpleRule('rule-1'), - setup: 'A setup guide', - }); - - const ruleUpdate = { - ...getSimpleRuleUpdate('rule-1'), - setup: 'A different setup guide', - }; - - const { body } = await securitySolutionApi.updateRule({ body: ruleUpdate }).expect(200); - - expect(body.setup).to.eql('A different setup guide'); - }); - - it('should reset setup field to empty string on unset', async () => { - await createRule(supertest, log, { - ...getSimpleRule('rule-1'), - setup: 'A setup guide', - }); - - const ruleUpdate = { - ...getSimpleRuleUpdate('rule-1'), - setup: undefined, - }; - - const { body } = await securitySolutionApi.updateRule({ body: ruleUpdate }).expect(200); - - expect(body.setup).to.eql(''); - }); - }); }); }); }; diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 2a7dad8cd8559..588843b731f45 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -44,6 +44,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // See https://github.com/elastic/kibana/pull/125396 for details '--xpack.alerting.rules.minimumScheduleInterval.value=1s', '--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true', + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'bulkCustomHighlightedFieldsEnabled', + ])}`, // mock cloud to enable the guided onboarding tour in e2e tests '--xpack.cloud.id=test', `--home.disableWelcomeScreen=true`, diff --git a/x-pack/test/security_solution_cypress/cypress/data/detection_engine.ts b/x-pack/test/security_solution_cypress/cypress/data/detection_engine.ts index f50f2478537f4..60ebea7632b50 100644 --- a/x-pack/test/security_solution_cypress/cypress/data/detection_engine.ts +++ b/x-pack/test/security_solution_cypress/cypress/data/detection_engine.ts @@ -25,6 +25,7 @@ import type { RuleName, RuleReferenceArray, RuleTagArray, + SetupGuide, } from '@kbn/security-solution-plugin/common/api/detection_engine'; interface RuleFields { @@ -44,6 +45,7 @@ interface RuleFields { threat: Threat; threatSubtechnique: ThreatSubtechnique; threatTechnique: ThreatTechnique; + setup: SetupGuide; } export const ruleFields: RuleFields = { @@ -60,6 +62,7 @@ export const ruleFields: RuleFields = { ], falsePositives: ['False1', 'False2'], investigationGuide: '# test markdown', + setup: '# test setup markdown', investigationFields: { field_names: ['agent.hostname'], }, diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/common_flows.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/common_flows.cy.ts index a5903af58f1ee..c718930cdf71e 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/common_flows.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/common_flows.cy.ts @@ -15,7 +15,11 @@ import { RULE_NAME_INPUT, SCHEDULE_CONTINUE_BUTTON, } from '../../../../screens/create_new_rule'; -import { RULE_NAME_HEADER } from '../../../../screens/rule_details'; +import { + DESCRIPTION_SETUP_GUIDE_BUTTON, + DESCRIPTION_SETUP_GUIDE_CONTENT, + RULE_NAME_HEADER, +} from '../../../../screens/rule_details'; import { createTimeline } from '../../../../tasks/api_calls/timelines'; import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; import { @@ -31,6 +35,7 @@ import { fillRiskScore, fillRuleName, fillRuleTags, + fillSetup, fillSeverity, fillThreat, fillThreatSubtechnique, @@ -77,6 +82,7 @@ describe('Common rule creation flows', { tags: ['@ess', '@serverless'] }, () => fillThreatSubtechnique(); fillCustomInvestigationFields(); fillNote(); + fillSetup(); cy.get(ABOUT_CONTINUE_BTN).click(); cy.log('Filling schedule section'); @@ -97,5 +103,8 @@ describe('Common rule creation flows', { tags: ['@ess', '@serverless'] }, () => // UI redirects to rule creation page of a created rule cy.get(RULE_NAME_HEADER).should('contain', ruleFields.ruleName); + + cy.get(DESCRIPTION_SETUP_GUIDE_BUTTON).click(); + cy.get(DESCRIPTION_SETUP_GUIDE_CONTENT).should('contain', 'test setup markdown'); // Markdown formatting should be removed }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule_serverless.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule_serverless.cy.ts index f2b2b07975a04..95e95ca5e1439 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule_serverless.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule_serverless.cy.ts @@ -20,25 +20,19 @@ describe('Detection ES|QL rules, creation', { tags: ['@serverless'] }, () => { login(); }); - it('does not display ES|QL rule on form', function () { + it('should display ES|QL rule on form', function () { visit(CREATE_RULE_URL); // ensure, page is loaded and rule types are displayed cy.get(NEW_TERMS_TYPE).should('be.visible'); cy.get(THRESHOLD_TYPE).should('be.visible'); - // ES|QL rule tile should not be rendered - cy.get(ESQL_TYPE).should('not.exist'); + cy.get(ESQL_TYPE).should('exist'); }); - it('does not allow to create rule by API call', function () { + it('allow creation rule by API call', function () { createRule(getEsqlRule()).then((response) => { - expect(response.status).to.equal(400); - - expect(response.body).to.deep.equal({ - status_code: 400, - message: 'Rule type "siem.esqlRule" is not registered.', - }); + expect(response.status).to.equal(200); }); }); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_authorization.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_authorization.cy.ts index c9349ea6d083c..bc0b531a156f1 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_authorization.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_authorization.cy.ts @@ -66,7 +66,10 @@ const loginPageAsWriteAuthorizedUser = (url: string) => { }; // https://github.com/elastic/kibana/issues/179965 -describe( +// Failing: See https://github.com/elastic/kibana/issues/182485 +// Failing: See https://github.com/elastic/kibana/issues/182483 +// Failing: See https://github.com/elastic/kibana/issues/182486 +describe.skip( 'Detection rules, Prebuilt Rules Installation and Update - Authorization/RBAC', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts index 762e79bb27003..29ded745c05b7 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_via_fleet.cy.ts @@ -16,7 +16,9 @@ import { clickAddElasticRulesButton } from '../../../../tasks/prebuilt_rules'; import { visitRulesManagementTable } from '../../../../tasks/rules_management'; import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; -describe( +// Failing: See https://github.com/elastic/kibana/issues/182439 +// Failing: See https://github.com/elastic/kibana/issues/182440 +describe.skip( 'Detection rules, Prebuilt Rules Installation and Update workflow', { tags: ['@ess', '@serverless'] }, () => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_workflow.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_workflow.cy.ts index 782672ccb1c45..75c83d0aa2dab 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_workflow.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_workflow.cy.ts @@ -30,7 +30,8 @@ import { import { visitRulesManagementTable } from '../../../../tasks/rules_management'; import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; -describe( +// Failing: See https://github.com/elastic/kibana/issues/182441 +describe.skip( 'Detection rules, Prebuilt Rules Installation and Update workflow', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/management.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/management.cy.ts index b49426b9b515a..35736e319737f 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/management.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/management.cy.ts @@ -51,7 +51,8 @@ const rules = Array.from(Array(5)).map((_, i) => { }); // https://github.com/elastic/kibana/issues/179973 -describe('Prebuilt rules', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { +// Failing: See https://github.com/elastic/kibana/issues/182442 +describe.skip('Prebuilt rules', { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { beforeEach(() => { login(); deleteAlertsAndRules(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules.cy.ts index 573fc2c556abe..4584c0d5719e5 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules.cy.ts @@ -20,9 +20,13 @@ import { TAGS_RULE_BULK_MENU_ITEM, INDEX_PATTERNS_RULE_BULK_MENU_ITEM, APPLY_TIMELINE_RULE_BULK_MENU_ITEM, + RULES_BULK_EDIT_INVESTIGATION_FIELDS_WARNING, } from '../../../../../screens/rules_bulk_actions'; -import { TIMELINE_TEMPLATE_DETAILS } from '../../../../../screens/rule_details'; +import { + INVESTIGATION_FIELDS_DETAILS, + TIMELINE_TEMPLATE_DETAILS, +} from '../../../../../screens/rule_details'; import { EUI_CHECKBOX, EUI_FILTER_SELECT_ITEM } from '../../../../../screens/common/controls'; @@ -72,10 +76,19 @@ import { assertRuleScheduleValues, assertUpdateScheduleWarningExists, assertDefaultValuesAreAppliedToScheduleFields, + openBulkEditAddInvestigationFieldsForm, + typeInvestigationFields, + checkOverwriteInvestigationFieldsCheckbox, + openBulkEditDeleteInvestigationFieldsForm, } from '../../../../../tasks/rules_bulk_actions'; import { createRuleAssetSavedObject } from '../../../../../helpers/rules'; -import { hasIndexPatterns, getDetails } from '../../../../../tasks/rule_details'; +import { + hasIndexPatterns, + getDetails, + hasInvestigationFields, + assertDetailsNotExist, +} from '../../../../../tasks/rule_details'; import { login } from '../../../../../tasks/login'; import { visitRulesManagementTable } from '../../../../../tasks/rules_management'; import { createRule } from '../../../../../tasks/api_calls/rules'; @@ -102,14 +115,16 @@ import { setRowsPerPageTo, sortByTableColumn } from '../../../../../tasks/table_ const RULE_NAME = 'Custom rule for bulk actions'; const EUI_SELECTABLE_LIST_ITEM_SR_TEXT = '. To check this option, press Enter.'; -const prePopulatedIndexPatterns = ['index-1-*', 'index-2-*']; +const prePopulatedIndexPatterns = ['index-1-*', 'index-2-*', 'auditbeat-*']; const prePopulatedTags = ['test-default-tag-1', 'test-default-tag-2']; +const prePopulatedInvestigationFields = ['agent.version', 'host.name']; const expectedNumberOfMachineLearningRulesToBeEdited = 1; const defaultRuleData = { index: prePopulatedIndexPatterns, tags: prePopulatedTags, + investigation_fields: { field_names: prePopulatedInvestigationFields }, timeline_title: 'Generic Threat Match Timeline', timeline_id: '495ad7a7-316e-4544-8a0f-9c098daee76e', }; @@ -129,6 +144,7 @@ describe('Detection rules, bulk edit', { tags: ['@ess', '@serverless'] }, () => getMachineLearningRule({ name: 'New ML Rule Test', tags: ['test-default-tag-1', 'test-default-tag-2'], + investigation_fields: { field_names: prePopulatedInvestigationFields }, enabled: false, }) ); @@ -562,6 +578,86 @@ describe('Detection rules, bulk edit', { tags: ['@ess', '@serverless'] }, () => }); }); + describe('Investigation fields actions', () => { + it('Add investigation fields to custom rules', () => { + getRulesManagementTableRows().then((rows) => { + const fieldsToBeAdded = ['source.ip', 'destination.ip']; + const resultingFields = [...prePopulatedInvestigationFields, ...fieldsToBeAdded]; + + selectAllRules(); + + // open add custom highlighted fields form and add 2 new fields + openBulkEditAddInvestigationFieldsForm(); + typeInvestigationFields(fieldsToBeAdded); + submitBulkEditForm(); + waitForBulkEditActionToFinish({ updatedCount: rows.length }); + + // check if rule has been updated + goToRuleDetailsOf(RULE_NAME); + hasInvestigationFields(resultingFields.join('')); + }); + }); + + it('Overwrite investigation fields in custom rules', () => { + getRulesManagementTableRows().then((rows) => { + const fieldsToOverwrite = ['source.ip']; + + selectAllRules(); + + // open add tags form, check overwrite tags and warning message, type tags + openBulkEditAddInvestigationFieldsForm(); + checkOverwriteInvestigationFieldsCheckbox(); + + cy.get(RULES_BULK_EDIT_INVESTIGATION_FIELDS_WARNING).should( + 'have.text', + `You’re about to overwrite custom highlighted fields for ${rows.length} selected rules, press Save to apply changes.` + ); + + typeInvestigationFields(fieldsToOverwrite); + submitBulkEditForm(); + waitForBulkEditActionToFinish({ updatedCount: rows.length }); + + // check if rule has been updated + goToRuleDetailsOf(RULE_NAME); + hasInvestigationFields(fieldsToOverwrite.join('')); + }); + }); + + it('Delete investigation fields from custom rules', () => { + getRulesManagementTableRows().then((rows) => { + const fieldsToDelete = prePopulatedInvestigationFields.slice(0, 1); + const resultingFields = prePopulatedInvestigationFields.slice(1); + + selectAllRules(); + + // open add tags form, check overwrite tags, type tags + openBulkEditDeleteInvestigationFieldsForm(); + typeInvestigationFields(fieldsToDelete); + submitBulkEditForm(); + waitForBulkEditActionToFinish({ updatedCount: rows.length }); + + // check if rule has been updated + goToRuleDetailsOf(RULE_NAME); + hasInvestigationFields(resultingFields.join('')); + }); + }); + + it('Delete all investigation fields from custom rules', () => { + getRulesManagementTableRows().then((rows) => { + selectAllRules(); + + openBulkEditDeleteInvestigationFieldsForm(); + typeInvestigationFields(prePopulatedInvestigationFields); + submitBulkEditForm(); + waitForBulkEditActionToFinish({ updatedCount: rows.length }); + + // check if rule has been updated + goToRuleDetailsOf(RULE_NAME); + assertDetailsNotExist(INVESTIGATION_FIELDS_DETAILS); + }); + }); + }); + describe('Timeline templates', () => { beforeEach(() => { loadPrepackagedTimelineTemplates(); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts index bf88869973cee..d9b70b1ddd4e4 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts @@ -135,6 +135,8 @@ export const INPUT = '[data-test-subj="input"]'; export const INVESTIGATION_NOTES_TEXTAREA = '[data-test-subj="detectionEngineStepAboutRuleNote"] textarea'; +export const SETUP_GUIDE_TEXTAREA = '[data-test-subj="detectionEngineStepAboutRuleSetup"] textarea'; + export const FALSE_POSITIVES_INPUT = '[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] input'; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts b/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts index b8c524b0084ce..d67a07faf6079 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/rule_details.ts @@ -54,6 +54,8 @@ export const FALSE_POSITIVES_DETAILS = 'False positive examples'; export const INDEX_PATTERNS_DETAILS = 'Index patterns'; +export const INVESTIGATION_FIELDS_DETAILS = 'Custom highlighted fields'; + export const ENDPOINT_EXCEPTIONS_TAB = 'a[data-test-subj="navigation-endpoint_exceptions"]'; export const INDICATOR_INDEX_PATTERNS = 'Indicator index patterns'; @@ -153,3 +155,7 @@ export const ALERT_SUPPRESSION_INSUFFICIENT_LICENSING_ICON = export const HIGHLIGHTED_ROWS_IN_TABLE = '[data-test-subj="euiDataGridBody"] .alertsTableHighlightedRow'; + +export const DESCRIPTION_SETUP_GUIDE_BUTTON = '[data-test-subj="stepAboutDetailsToggle-setup"]'; + +export const DESCRIPTION_SETUP_GUIDE_CONTENT = '[data-test-subj="stepAboutDetailsSetupContent"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/screens/rules_bulk_actions.ts b/x-pack/test/security_solution_cypress/cypress/screens/rules_bulk_actions.ts index 69efa1d31c2a7..cdf458a1e9ad8 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/rules_bulk_actions.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/rules_bulk_actions.ts @@ -83,6 +83,25 @@ export const RULES_BULK_EDIT_OVERWRITE_TAGS_CHECKBOX = export const RULES_BULK_EDIT_TAGS_WARNING = '[data-test-subj="bulkEditRulesTagsWarning"]'; +// INVESTIGATION FIELDS +export const INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM = + '[data-test-subj="investigationFieldsBulkEditRule"]'; + +export const ADD_INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM = + '[data-test-subj="addInvestigationFieldsBulkEditRule"]'; + +export const DELETE_INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM = + '[data-test-subj="deleteInvestigationFieldsBulkEditRule"]'; + +export const RULES_BULK_EDIT_INVESTIGATION_FIELDS = + '[data-test-subj="bulkEditRulesInvestigationFields"]'; + +export const RULES_BULK_EDIT_OVERWRITE_INVESTIGATION_FIELDS_CHECKBOX = + '[data-test-subj="bulkEditRulesOverwriteInvestigationFields"]'; + +export const RULES_BULK_EDIT_INVESTIGATION_FIELDS_WARNING = + '[data-test-subj="bulkEditRulesInvestigationFieldsWarning"]'; + // ENABLE/DISABLE export const ENABLE_RULE_BULK_BTN = '[data-test-subj="enableRuleBulk"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts index f40cecee5a981..aa97035eddc47 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts @@ -125,6 +125,7 @@ import { ALERTS_INDEX_BUTTON, INVESTIGATIONS_INPUT, QUERY_BAR_ADD_FILTER, + SETUP_GUIDE_TEXTAREA, RELATED_INTEGRATION_COMBO_BOX_INPUT, } from '../screens/create_new_rule'; import { @@ -204,6 +205,13 @@ export const fillNote = (note: string = ruleFields.investigationGuide) => { return note; }; +export const fillSetup = (setup: string = ruleFields.setup) => { + cy.get(SETUP_GUIDE_TEXTAREA).clear({ force: true }); + cy.get(SETUP_GUIDE_TEXTAREA).type(setup); + + return setup; +}; + export const fillMitre = (mitreAttacks: Threat[]) => { let techniqueIndex = 0; let subtechniqueInputIndex = 0; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/rule_details.ts b/x-pack/test/security_solution_cypress/cypress/tasks/rule_details.ts index 172408aa677c4..b5b82d78783c4 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/rule_details.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/rule_details.ts @@ -34,6 +34,8 @@ import { EXCEPTIONS_TAB_EXPIRED_FILTER, EXCEPTIONS_TAB_ACTIVE_FILTER, RULE_NAME_HEADER, + INVESTIGATION_FIELDS_DETAILS, + ABOUT_DETAILS, } from '../screens/rule_details'; import { RuleDetailsTabs, ruleDetailsUrl } from '../urls/rule_details'; import { @@ -179,6 +181,12 @@ export const hasIndexPatterns = (indexPatterns: string) => { }); }; +export const hasInvestigationFields = (fields: string) => { + cy.get(ABOUT_DETAILS).within(() => { + getDetails(INVESTIGATION_FIELDS_DETAILS).should('have.text', fields); + }); +}; + export const goToRuleEditSettings = () => { cy.get(EDIT_RULE_SETTINGS_LINK).click(); }; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/rules_bulk_actions.ts b/x-pack/test/security_solution_cypress/cypress/tasks/rules_bulk_actions.ts index 79960f6aa7464..c74214c60fbac 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/rules_bulk_actions.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/rules_bulk_actions.ts @@ -21,6 +21,7 @@ import { import { EUI_SELECTABLE_LIST_ITEM, TIMELINE_SEARCHBOX } from '../screens/common/controls'; import { ADD_INDEX_PATTERNS_RULE_BULK_MENU_ITEM, + ADD_INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM, ADD_RULE_ACTIONS_MENU_ITEM, ADD_TAGS_RULE_BULK_MENU_ITEM, APPLY_TIMELINE_RULE_BULK_MENU_ITEM, @@ -28,18 +29,22 @@ import { BULK_ACTIONS_PROGRESS_BTN, BULK_EXPORT_ACTION_BTN, DELETE_INDEX_PATTERNS_RULE_BULK_MENU_ITEM, + DELETE_INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM, DELETE_RULE_BULK_BTN, DELETE_TAGS_RULE_BULK_MENU_ITEM, DISABLE_RULE_BULK_BTN, DUPLICATE_RULE_BULK_BTN, ENABLE_RULE_BULK_BTN, INDEX_PATTERNS_RULE_BULK_MENU_ITEM, + INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM, RULES_BULK_EDIT_FORM_CONFIRM_BTN, RULES_BULK_EDIT_FORM_TITLE, RULES_BULK_EDIT_INDEX_PATTERNS, + RULES_BULK_EDIT_INVESTIGATION_FIELDS, RULES_BULK_EDIT_OVERWRITE_ACTIONS_CHECKBOX, RULES_BULK_EDIT_OVERWRITE_DATA_VIEW_CHECKBOX, RULES_BULK_EDIT_OVERWRITE_INDEX_PATTERNS_CHECKBOX, + RULES_BULK_EDIT_OVERWRITE_INVESTIGATION_FIELDS_CHECKBOX, RULES_BULK_EDIT_OVERWRITE_TAGS_CHECKBOX, RULES_BULK_EDIT_SCHEDULES_WARNING, RULES_BULK_EDIT_TAGS, @@ -232,6 +237,46 @@ export const checkTagsInTagsFilter = (tags: string[], srOnlyText: string = '') = }); }; +// EDIT-INVESTIGATION FIELDS +const clickInvestigationFieldsMenuItem = () => { + cy.get(BULK_ACTIONS_BTN).click(); + cy.get(INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM).click(); +}; + +export const clickAddInvestigationFieldsMenuItem = () => { + clickInvestigationFieldsMenuItem(); + cy.get(ADD_INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM).click(); +}; + +export const openBulkEditAddInvestigationFieldsForm = () => { + clickAddInvestigationFieldsMenuItem(); + + cy.get(RULES_BULK_EDIT_FORM_TITLE).should('have.text', 'Add custom highlighted fields'); +}; + +export const openBulkEditDeleteInvestigationFieldsForm = () => { + clickInvestigationFieldsMenuItem(); + cy.get(DELETE_INVESTIGATION_FIELDS_RULE_BULK_MENU_ITEM).click(); + + cy.get(RULES_BULK_EDIT_FORM_TITLE).should('have.text', 'Delete custom highlighted fields'); +}; + +export const typeInvestigationFields = (fields: string[]) => { + cy.get(RULES_BULK_EDIT_INVESTIGATION_FIELDS) + .find('input') + .type(fields.join('{enter}') + '{enter}'); +}; + +export const checkOverwriteInvestigationFieldsCheckbox = () => { + cy.get(RULES_BULK_EDIT_OVERWRITE_INVESTIGATION_FIELDS_CHECKBOX) + .should('have.text', "Overwrite all selected rules' custom highlighted fields") + .click(); + cy.get(RULES_BULK_EDIT_OVERWRITE_INVESTIGATION_FIELDS_CHECKBOX) + .should('have.text', "Overwrite all selected rules' custom highlighted fields") + .get('input') + .should('be.checked'); +}; + // EDIT-SCHEDULE export const clickUpdateScheduleMenuItem = () => { cy.get(BULK_ACTIONS_BTN).click(); diff --git a/x-pack/test/security_solution_cypress/serverless_config.ts b/x-pack/test/security_solution_cypress/serverless_config.ts index d0ee1613f6e4c..51462be717fc8 100644 --- a/x-pack/test/security_solution_cypress/serverless_config.ts +++ b/x-pack/test/security_solution_cypress/serverless_config.ts @@ -34,6 +34,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { { product_line: 'endpoint', product_tier: 'complete' }, { product_line: 'cloud', product_tier: 'complete' }, ])}`, + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'bulkCustomHighlightedFieldsEnabled', + ])}`, ], }, testRunner: SecuritySolutionConfigurableCypressTestRunner,