From 043d780b2afbfc722c6f849bc7dfdb1098550447 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Mon, 3 Mar 2025 14:41:40 +0100 Subject: [PATCH 1/9] [scout] enable ES authc debug logs on ES start (#212866) ## Summary In #211055 we enabled Elasticsearch Authc debug logs, but we use the different entry function to start servers on CI. This PR moves the call into `runElasticsearch` so that logs are enabled for every Scout script (start-server or run-tests) --- .../kbn-scout/src/common/services/clients.ts | 9 ++---- .../kbn-scout/src/common/utils/index.ts | 14 +++++++++- .../src/servers/run_elasticsearch.ts | 28 ++++++++++++++++++- .../kbn-scout/src/servers/start_servers.ts | 10 +------ 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/platform/packages/shared/kbn-scout/src/common/services/clients.ts b/src/platform/packages/shared/kbn-scout/src/common/services/clients.ts index ed5efd8e77786..20018ec9584e4 100644 --- a/src/platform/packages/shared/kbn-scout/src/common/services/clients.ts +++ b/src/platform/packages/shared/kbn-scout/src/common/services/clients.ts @@ -8,7 +8,6 @@ */ import { createEsClientForTesting, KbnClient } from '@kbn/test'; -import { ToolingLog } from '@kbn/tooling-log'; import { ScoutLogger } from './logger'; import { ScoutTestConfig, EsClient } from '../../types'; @@ -17,7 +16,7 @@ interface ClientOptions { url: string; username: string; password: string; - log: ScoutLogger | ToolingLog; + log: ScoutLogger; } function createClientUrlWithAuth({ serviceName, url, username, password, log }: ClientOptions) { @@ -25,9 +24,7 @@ function createClientUrlWithAuth({ serviceName, url, username, password, log }: clientUrl.username = username; clientUrl.password = password; - if (log instanceof ScoutLogger) { - log.serviceLoaded(`${serviceName}Client`); - } + log.serviceLoaded(`${serviceName}Client`); return clientUrl.toString(); } @@ -35,7 +32,7 @@ function createClientUrlWithAuth({ serviceName, url, username, password, log }: let esClientInstance: EsClient | null = null; let kbnClientInstance: KbnClient | null = null; -export function getEsClient(config: ScoutTestConfig, log: ScoutLogger | ToolingLog) { +export function getEsClient(config: ScoutTestConfig, log: ScoutLogger) { if (!esClientInstance) { const { username, password } = config.auth; const elasticsearchUrl = createClientUrlWithAuth({ diff --git a/src/platform/packages/shared/kbn-scout/src/common/utils/index.ts b/src/platform/packages/shared/kbn-scout/src/common/utils/index.ts index ab112412b64a1..5829f7eeaf2e2 100644 --- a/src/platform/packages/shared/kbn-scout/src/common/utils/index.ts +++ b/src/platform/packages/shared/kbn-scout/src/common/utils/index.ts @@ -14,7 +14,19 @@ export async function silence(log: ToolingLog, milliseconds: number) { await Rx.firstValueFrom( log.getWritten$().pipe( Rx.startWith(null), - Rx.switchMap(() => Rx.timer(milliseconds)) + Rx.switchMap((message) => { + if ( + // TODO: remove workaround to ignore ES authc debug logs for stateful run + message?.args[0]?.includes( + 'Authentication of [kibana_system] using realm [reserved/reserved]' + ) || + message?.args[0]?.includes('realm [reserved] authenticated user [kibana_system]') + ) { + return Rx.of(null); + } else { + return Rx.timer(milliseconds); + } + }) ) ); } diff --git a/src/platform/packages/shared/kbn-scout/src/servers/run_elasticsearch.ts b/src/platform/packages/shared/kbn-scout/src/servers/run_elasticsearch.ts index 24c8a49da2d9a..1179cd090769c 100644 --- a/src/platform/packages/shared/kbn-scout/src/servers/run_elasticsearch.ts +++ b/src/platform/packages/shared/kbn-scout/src/servers/run_elasticsearch.ts @@ -13,7 +13,12 @@ import type { ToolingLog } from '@kbn/tooling-log'; import { REPO_ROOT } from '@kbn/repo-info'; import type { ArtifactLicense, ServerlessProjectType } from '@kbn/es'; import { isServerlessProjectType } from '@kbn/es/src/utils'; -import { createTestEsCluster, esTestConfig, cleanupElasticsearch } from '@kbn/test'; +import { + createTestEsCluster, + esTestConfig, + cleanupElasticsearch, + createEsClientForTesting, +} from '@kbn/test'; import { Config } from '../config'; interface RunElasticsearchOptions { @@ -81,6 +86,27 @@ export async function runElasticsearch( logsDir, config, }); + + // TODO: Remove this once we find out why SAML callback randomly fails with 401 + log.info('Enable authc debug logs for ES'); + const clientUrl = new URL( + Url.format({ + protocol: options.config.get('servers.elasticsearch.protocol'), + hostname: options.config.get('servers.elasticsearch.hostname'), + port: options.config.get('servers.elasticsearch.port'), + }) + ); + clientUrl.username = options.config.get('servers.kibana.username'); + clientUrl.password = options.config.get('servers.kibana.password'); + const esClient = createEsClientForTesting({ + esUrl: clientUrl.toString(), + }); + await esClient.cluster.putSettings({ + persistent: { + 'logger.org.elasticsearch.xpack.security.authc': 'debug', + }, + }); + return async () => { await cleanupElasticsearch(node, config.serverless, logsDir, log); }; diff --git a/src/platform/packages/shared/kbn-scout/src/servers/start_servers.ts b/src/platform/packages/shared/kbn-scout/src/servers/start_servers.ts index 876f60f18f02e..32eb2030c978d 100644 --- a/src/platform/packages/shared/kbn-scout/src/servers/start_servers.ts +++ b/src/platform/packages/shared/kbn-scout/src/servers/start_servers.ts @@ -16,7 +16,7 @@ import { runElasticsearch } from './run_elasticsearch'; import { getExtraKbnOpts, runKibanaServer } from './run_kibana_server'; import { StartServerOptions } from './flags'; import { loadServersConfig } from '../config'; -import { getEsClient, silence } from '../common'; +import { silence } from '../common'; export async function startServers(log: ToolingLog, options: StartServerOptions) { const runStartTime = Date.now(); @@ -32,14 +32,6 @@ export async function startServers(log: ToolingLog, options: StartServerOptions) logsDir: options.logsDir, }); - log.info('Enable authc debug logs for ES'); - const client = getEsClient(config.getScoutTestConfig(), log); - await client.cluster.putSettings({ - persistent: { - 'logger.org.elasticsearch.xpack.security.authc': 'debug', - }, - }); - await runKibanaServer({ procs, config, From ab44603a1ccc0ea1a697fdb3b80e9705edcb7eea Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 3 Mar 2025 08:48:43 -0500 Subject: [PATCH 2/9] [Chore] update form data to ^4.0.2 (#212795) ## Summary Updates form-data to ^4.0.2 Relates to https://github.com/elastic/kibana/pull/212183 form-data is used by data forge to load kibana assets. Ran data forge with `--install-kibana-assets` to smoke test. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 111 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 73 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index b1daf74591c88..d631a0509391e 100644 --- a/package.json +++ b/package.json @@ -1767,7 +1767,7 @@ "fetch-mock": "^10.1.0", "file-loader": "^4.2.0", "find-cypress-specs": "^1.41.4", - "form-data": "^4.0.1", + "form-data": "^4.0.2", "geckodriver": "^5.0.0", "gulp-brotli": "^3.0.0", "gulp-postcss": "^9.0.1", diff --git a/yarn.lock b/yarn.lock index b6c951a71f852..71c6f152b1f13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15213,6 +15213,14 @@ caching-transform@^4.0.0: package-hash "^4.0.0" write-file-atomic "^3.0.0" +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -17955,6 +17963,15 @@ downshift@^3.2.10: prop-types "^15.7.2" react-is "^16.9.0" +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + duplexer2@^0.1.2: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" @@ -18358,12 +18375,10 @@ es-array-method-boxes-properly@^1.0.0: resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" @@ -18390,21 +18405,22 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.0.tgz#4878fee3789ad99e065f975fdd3c645529ff0236" integrity sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw== -es-object-atoms@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" - integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== dependencies: es-errors "^1.3.0" -es-set-tostringtag@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" - integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== +es-set-tostringtag@^2.0.3, es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== dependencies: - get-intrinsic "^1.2.4" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" has-tostringtag "^1.0.2" - hasown "^2.0.1" + hasown "^2.0.2" es-shim-unscopables@^1.0.0: version "1.0.0" @@ -19787,13 +19803,14 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@^4.0.0, form-data@^4.0.1, form-data@~4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" - integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== +form-data@^4.0.0, form-data@^4.0.2, form-data@~4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c" + integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" mime-types "^2.1.12" formdata-node@^4.3.2: @@ -20109,16 +20126,21 @@ get-east-asian-width@^1.0.0: resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4, get-intrinsic@^1.2.6: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" es-errors "^1.3.0" + es-object-atoms "^1.1.1" function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" get-nonce@^1.0.0: version "1.0.1" @@ -20145,6 +20167,14 @@ get-port@^5.0.0: resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" @@ -20527,12 +20557,10 @@ google-protobuf@^3.6.1: resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.19.4.tgz#8d32c3e34be9250956f28c0fb90955d13f311888" integrity sha512-OIPNCxsG2lkIvf+P5FNfJ/Km95CsXOBecS9ZcAU6m2Rq3svc0Apl9nB3GMDNKfQ9asNv4KjyAqGwPQFrVle3Yg== -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== got@^11.8.2: version "11.8.5" @@ -20710,15 +20738,15 @@ has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: dependencies: es-define-property "^1.0.0" -has-proto@^1.0.1, has-proto@^1.0.3: +has-proto@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== -has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" @@ -20799,7 +20827,7 @@ hasha@^5.0.0: is-stream "^2.0.0" type-fest "^0.8.0" -hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: +hasown@^2.0.0, hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== @@ -24279,6 +24307,11 @@ marked@^4.3.0: resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + mathml-tag-names@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" From c4a016eda30ae8f224fdd485a634dc6773898e31 Mon Sep 17 00:00:00 2001 From: Dmitrii Shevchenko Date: Mon, 3 Mar 2025 15:03:07 +0100 Subject: [PATCH 3/9] [Security Solution] Reduce the _review rule upgrade endpoint response size (#211045) **Resolves: https://github.com/elastic/kibana/issues/208361** **Resolves: https://github.com/elastic/kibana/issues/210544** ## Summary This PR introduces significant memory consumption improvements to the prebuilt rule endpoints, ensuring users won't encounter OOM errors on memory-limited Kibana instances. Memory consumption testing results provided in https://github.com/elastic/kibana/pull/211045#issuecomment-2689854328. ## Details This PR implements a number of memory usage optimizations to the prebuilt rule endpoints with the final goal reducing chances of getting OOM errors. The changes are extensive and require thorough testing before merging. The changes are described by the following bullets - The most significant change is the addition of pagination to the `upgrade/_review` endpoint. This endpoint was known for causing OOM errors due to its large and ever-growing response size. With pagination, it now returns upgrade information for no more than 20-100 rules at a time, significantly reducing its memory footprint. - New backend methods, such as `ruleObjectsClient.fetchInstalledRuleVersions`, have been introduced. These methods return rule IDs with their corresponding installed versions, allowing to build a map of outdated rules without loading all available rules into memory. Previously, all installed rules, along with their base and target versions, were fetched unconditionally before filtering for updates. - The `stats` data structure of the review endpoint has been deprecated (it can be safely removed after one Serverless release cycle). Since the endpoint now returns paginated results, building stats is no longer feasible due to the limited rule set size fetched on the server side. As the side effect it required removing related Cypress tests asserting `Update All` disabled when rules can't be updated. - All changes to the endpoints are backward-compatible. All previously required returned structures still present in response. All newly added structures are optional. - Upgradeable rule tags are now returned from the prebuilt rule status endpoint. - The frontend logic has been updated to move sorting and filtering of prebuilt rules from the client side to the server side. - The `upgrade/_perform` endpoint has been rewritten to use lightweight rule version information rather than full rules to determine upgradeable rules. Additionally, upgrades are now performed in batches of up to 100 rules, further reducing memory usage. - A dry run option has been added to the upgrade perform endpoint. This is needed for the "Update all" rules scenario to determine if any rules contain conflicts and display a confirmation modal to the user. - An option to skip conflicting rules has been added to the upgrade endpoint when called with the `ALL_RULES` mode. - The `install/_review` endpoint's memory consumption has been optimized by avoiding loading all rules into memory to determine available rules for installation. Redundant fetching of all base versions has also been removed, as they do not participate in the calculation. --------- Co-authored-by: Maxim Palenov --- .../common/prebuilt_rules_filter.ts | 29 +++ .../review_prebuilt_rules_upgrade_filter.ts | 24 ++ .../get_prebuilt_rules_status_route.ts | 12 + .../detection_engine/prebuilt_rules/index.ts | 1 + .../perform_rule_upgrade_route.ts | 13 +- .../review_rule_upgrade_route.ts | 65 ++++- .../security_solution/common/constants.ts | 10 +- .../rule_management/rule_fields.ts | 1 + .../rule_management/rule_filtering.ts | 13 +- .../rule_management/api/api.ts | 25 +- .../use_fetch_prebuilt_rules_status_query.ts | 12 +- ...tch_prebuilt_rules_upgrade_review_query.ts | 10 +- ... => use_perform_rules_upgrade_mutation.ts} | 30 +-- .../use_perform_rule_upgrade.ts | 12 +- .../use_prebuilt_rules_upgrade_review.ts | 8 +- .../rule_management/logic/types.ts | 12 +- .../rule_update_callouts.tsx | 4 +- .../add_prebuilt_rules_table_context.tsx | 2 +- .../rules_table/rules_table_toolbar.tsx | 2 +- .../translations.tsx | 7 - .../upgrade_prebuilt_rules_table.tsx | 43 +++- .../upgrade_prebuilt_rules_table_buttons.tsx | 28 +-- .../upgrade_prebuilt_rules_table_context.tsx | 202 +++++++++++---- .../upgrade_prebuilt_rules_table_filters.tsx | 36 ++- ...rade_rule_customization_filter_popover.tsx | 35 ++- .../use_filter_prebuilt_rules_to_upgrade.ts | 48 ---- .../use_prebuilt_rules_upgrade_state.test.ts | 1 + .../add_elastic_rules_button.tsx | 2 +- .../get_prebuilt_rules_status_route.ts | 30 ++- .../get_upgradeable_rules.test.ts | 191 -------------- .../get_upgradeable_rules.ts | 85 ------- .../perform_rule_upgrade_handler.ts | 235 +++++++++++++++++ .../perform_rule_upgrade_route.ts | 109 +------- .../review_rule_installation_handler.ts | 20 +- .../calculate_rule_upgrade_info.ts | 59 +++++ .../review_rule_upgrade_handler.ts | 170 +++++++------ .../review_rule_upgrade_route.ts | 18 +- .../prebuilt_rules/constants.ts | 8 + .../prebuilt_rule_objects_client.ts | 136 +++++++++- .../fetch_rule_versions_triad.ts | 16 +- .../search/get_existing_prepackaged_rules.ts | 14 +- .../preview_prebuilt_rules_upgrade.ts | 236 ------------------ .../update_prebuilt_rules_package.ts | 11 - .../install_update_error_handling.cy.ts | 6 + .../update_workflow_customized_rules.cy.ts | 8 - .../cypress/tasks/api_calls/prebuilt_rules.ts | 12 +- .../cypress/tasks/prebuilt_rules.ts | 11 +- 47 files changed, 1045 insertions(+), 1017 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/prebuilt_rules_filter.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/review_prebuilt_rules_upgrade_filter.ts rename x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/{use_perform_specific_rules_upgrade_mutation.ts => use_perform_rules_upgrade_mutation.ts} (79%) delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.test.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/prebuilt_rules_filter.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/prebuilt_rules_filter.ts new file mode 100644 index 0000000000000..12a5fc449f571 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/prebuilt_rules_filter.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z } from '@kbn/zod'; + +export enum RuleCustomizationStatus { + CUSTOMIZED = 'CUSTOMIZED', + NOT_CUSTOMIZED = 'NOT_CUSTOMIZED', +} + +export type PrebuiltRulesFilter = z.infer; +export const PrebuiltRulesFilter = z.object({ + /** + * Tags to filter by + */ + tags: z.array(z.string()).optional(), + /** + * Rule name to filter by + */ + name: z.string().optional(), + /** + * Rule customization status to filter by + */ + customization_status: z.nativeEnum(RuleCustomizationStatus).optional(), +}); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/review_prebuilt_rules_upgrade_filter.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/review_prebuilt_rules_upgrade_filter.ts new file mode 100644 index 0000000000000..8b99489fb4bb4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/common/review_prebuilt_rules_upgrade_filter.ts @@ -0,0 +1,24 @@ +/* + * 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 { z } from '@kbn/zod'; +import { PrebuiltRulesFilter } from './prebuilt_rules_filter'; + +export enum RuleCustomizationStatus { + CUSTOMIZED = 'CUSTOMIZED', + NOT_CUSTOMIZED = 'NOT_CUSTOMIZED', +} + +export type ReviewPrebuiltRuleUpgradeFilter = z.infer; +export const ReviewPrebuiltRuleUpgradeFilter = PrebuiltRulesFilter.merge( + z.object({ + /** + * Rule IDs to return upgrade info for + */ + rule_ids: z.array(z.string()).optional(), + }) +); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts index e76ba63cfa17d..1a9e70f84cd2c 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts @@ -8,6 +8,18 @@ export interface GetPrebuiltRulesStatusResponseBody { /** Aggregated info about all prebuilt rules */ stats: PrebuiltRulesStatusStats; + + /** + * Aggregated info about upgradeable prebuilt rules. This fields is optional + * for backward compatibility. After one serverless release cycle, it can be + * made required. + * */ + aggregated_fields?: { + upgradeable_rules: { + /** List of all tags of the current versions of upgradeable rules */ + tags: string[]; + }; + }; } export interface PrebuiltRulesStatusStats { diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts index 02ab5c8a3cc0c..df1b5851f5474 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts @@ -22,3 +22,4 @@ export * from './model/diff/three_way_diff/three_way_diff_outcome'; export * from './model/diff/three_way_diff/three_way_diff'; export * from './model/diff/three_way_diff/three_way_diff_conflict'; export * from './model/diff/three_way_diff/three_way_merge_outcome'; +export * from './common/prebuilt_rules_filter'; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts index ac75bbb56e0bf..de3bb4fc27e1a 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts @@ -10,6 +10,7 @@ import { mapValues } from 'lodash'; import { RuleResponse } from '../../model/rule_schema/rule_schemas.gen'; import { AggregatedPrebuiltRuleError, DiffableAllFields } from '../model'; import { RuleSignatureId, RuleVersion } from '../../model'; +import { PrebuiltRulesFilter } from '../common/prebuilt_rules_filter'; export type Mode = z.infer; export const Mode = z.enum(['ALL_RULES', 'SPECIFIC_RULES']); @@ -111,21 +112,31 @@ export const RuleUpgradeSpecifier = z.object({ fields: RuleFieldsToUpgrade.optional(), }); +export type UpgradeConflictResolution = z.infer; +export const UpgradeConflictResolution = z.enum(['SKIP', 'OVERWRITE']); +export type UpgradeConflictResolutionEnum = typeof UpgradeConflictResolution.enum; +export const UpgradeConflictResolutionEnum = UpgradeConflictResolution.enum; + export type UpgradeSpecificRulesRequest = z.infer; export const UpgradeSpecificRulesRequest = z.object({ mode: z.literal('SPECIFIC_RULES'), rules: z.array(RuleUpgradeSpecifier).min(1), pick_version: PickVersionValues.optional(), + on_conflict: UpgradeConflictResolution.optional(), + dry_run: z.boolean().optional(), }); export type UpgradeAllRulesRequest = z.infer; export const UpgradeAllRulesRequest = z.object({ mode: z.literal('ALL_RULES'), pick_version: PickVersionValues.optional(), + filter: PrebuiltRulesFilter.optional(), + on_conflict: UpgradeConflictResolution.optional(), + dry_run: z.boolean().optional(), }); export type SkipRuleUpgradeReason = z.infer; -export const SkipRuleUpgradeReason = z.enum(['RULE_UP_TO_DATE']); +export const SkipRuleUpgradeReason = z.enum(['RULE_UP_TO_DATE', 'CONFLICT']); export type SkipRuleUpgradeReasonEnum = typeof SkipRuleUpgradeReason.enum; export const SkipRuleUpgradeReasonEnum = SkipRuleUpgradeReason.enum; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts index 2f2d6e3bd1c26..3fd95412b9311 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/detection_engine/prebuilt_rules/review_rule_upgrade/review_rule_upgrade_route.ts @@ -5,35 +5,86 @@ * 2.0. */ -import type { RuleObjectId, RuleSignatureId, RuleTagArray } from '../../model'; +import { z } from '@kbn/zod'; +import { SortOrder, type RuleObjectId, type RuleSignatureId, type RuleTagArray } from '../../model'; import type { PartialRuleDiff } from '../model'; -import type { RuleResponse } from '../../model/rule_schema'; +import type { RuleResponse, RuleVersion } from '../../model/rule_schema'; +import { FindRulesSortField } from '../../rule_management'; +import { PrebuiltRulesFilter } from '../common/prebuilt_rules_filter'; + +export type ReviewRuleUpgradeSort = z.infer; +export const ReviewRuleUpgradeSort = z.object({ + /** + * Field to sort by + */ + field: FindRulesSortField.optional(), + /** + * Sort order + */ + order: SortOrder.optional(), +}); + +export type ReviewRuleUpgradeRequestBody = z.infer; +export const ReviewRuleUpgradeRequestBody = z + .object({ + filter: PrebuiltRulesFilter.optional(), + sort: ReviewRuleUpgradeSort.optional(), + + page: z.coerce.number().int().min(1).optional().default(1), + /** + * Rules per page + */ + per_page: z.coerce.number().int().min(0).optional().default(20), + }) + .nullable(); export interface ReviewRuleUpgradeResponseBody { - /** Aggregated info about all rules available for upgrade */ + /** + * @deprecated Use the prebuilt rule status API instead. The field is kept + * here for backward compatibility but can be removed after one Serverless + * release. + */ stats: RuleUpgradeStatsForReview; /** Info about individual rules: one object per each rule available for upgrade */ rules: RuleUpgradeInfoForReview[]; + + /** The requested page number */ + page: number; + + /** The requested number of items per page */ + per_page: number; + + /** The total number of rules available for upgrade that match the filter criteria */ + total: number; } export interface RuleUpgradeStatsForReview { - /** Number of installed prebuilt rules available for upgrade (stock + customized) */ + /** + * @deprecated Always 0 + */ num_rules_to_upgrade_total: number; - /** Number of installed prebuilt rules with upgrade conflicts (SOLVABLE or NON_SOLVABLE) */ + /** + * @deprecated Always 0 + */ num_rules_with_conflicts: number; - /** Number of installed prebuilt rules with NON_SOLVABLE upgrade conflicts */ + /** + * @deprecated Always 0 + */ num_rules_with_non_solvable_conflicts: number; - /** A union of all tags of all rules available for upgrade */ + /** + * @deprecated Always an empty array + */ tags: RuleTagArray; } export interface RuleUpgradeInfoForReview { id: RuleObjectId; rule_id: RuleSignatureId; + version: RuleVersion; current_rule: RuleResponse; target_rule: RuleResponse; diff: PartialRuleDiff; diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index 7cf21a50307f2..b9923301a4cf6 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -388,7 +388,7 @@ export const STARTED_TRANSFORM_STATES = new Set([ ]); /** - * How many rules to update at a time is set to 50 from errors coming from + * How many rules to update at a time is set to 20 from errors coming from * the slow environments such as cloud when the rule updates are > 100 we were * seeing timeout issues. * @@ -403,14 +403,14 @@ export const STARTED_TRANSFORM_STATES = new Set([ * Lastly, we saw weird issues where Chrome on upstream 408 timeouts will re-call the REST route * which in turn could create additional connections we want to avoid. * - * See file import_rules_route.ts for another area where 50 was chosen, therefore I chose - * 50 here to mimic it as well. If you see this re-opened or what similar to it, consider - * reducing the 50 above to a lower number. + * See file import_rules_route.ts for another area where 20 was chosen, therefore I chose + * 20 here to mimic it as well. If you see this re-opened or what similar to it, consider + * reducing the 20 above to a lower number. * * See the original ticket here: * https://github.com/elastic/kibana/issues/94418 */ -export const MAX_RULES_TO_UPDATE_IN_PARALLEL = 50; +export const MAX_RULES_TO_UPDATE_IN_PARALLEL = 20; export const LIMITED_CONCURRENCY_ROUTE_TAG_PREFIX = `${APP_ID}:limitedConcurrency`; diff --git a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_fields.ts b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_fields.ts index 5d72cd15a96ae..610b2231e8ee7 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_fields.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_fields.ts @@ -22,3 +22,4 @@ export const TAGS_FIELD = 'alert.attributes.tags'; export const PARAMS_TYPE_FIELD = 'alert.attributes.params.type'; export const PARAMS_IMMUTABLE_FIELD = 'alert.attributes.params.immutable'; export const LAST_RUN_OUTCOME_FIELD = 'alert.attributes.lastRun.outcome'; +export const IS_CUSTOMIZED_FIELD = 'alert.attributes.params.ruleSource.isCustomized'; diff --git a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts index 52ace0cfac5da..692f2fa55a5e5 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts @@ -7,10 +7,11 @@ import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import type { RuleExecutionStatus } from '../../api/detection_engine'; -import { RuleExecutionStatusEnum } from '../../api/detection_engine'; +import { RuleCustomizationStatus, RuleExecutionStatusEnum } from '../../api/detection_engine'; import { prepareKQLStringParam } from '../../utils/kql'; import { ENABLED_FIELD, + IS_CUSTOMIZED_FIELD, LAST_RUN_OUTCOME_FIELD, PARAMS_IMMUTABLE_FIELD, PARAMS_TYPE_FIELD, @@ -23,6 +24,8 @@ export const KQL_FILTER_IMMUTABLE_RULES = `${PARAMS_IMMUTABLE_FIELD}: true`; export const KQL_FILTER_MUTABLE_RULES = `${PARAMS_IMMUTABLE_FIELD}: false`; export const KQL_FILTER_ENABLED_RULES = `${ENABLED_FIELD}: true`; export const KQL_FILTER_DISABLED_RULES = `${ENABLED_FIELD}: false`; +export const KQL_FILTER_CUSTOMIZED_RULES = `${IS_CUSTOMIZED_FIELD}: true`; +export const KQL_FILTER_NOT_CUSTOMIZED_RULES = `${IS_CUSTOMIZED_FIELD}: false`; interface RulesFilterOptions { filter: string; @@ -32,6 +35,7 @@ interface RulesFilterOptions { tags: string[]; excludeRuleTypes: Type[]; ruleExecutionStatus: RuleExecutionStatus; + customizationStatus: RuleCustomizationStatus; ruleIds: string[]; } @@ -50,6 +54,7 @@ export function convertRulesFilterToKQL({ tags, excludeRuleTypes = [], ruleExecutionStatus, + customizationStatus, }: Partial): string { const kql: string[] = []; @@ -85,6 +90,12 @@ export function convertRulesFilterToKQL({ kql.push(`${LAST_RUN_OUTCOME_FIELD}: "failed"`); } + if (customizationStatus === RuleCustomizationStatus.CUSTOMIZED) { + kql.push(KQL_FILTER_CUSTOMIZED_RULES); + } else if (customizationStatus === RuleCustomizationStatus.NOT_CUSTOMIZED) { + kql.push(KQL_FILTER_NOT_CUSTOMIZED_RULES); + } + return kql.join(' AND '); } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index 5f5fded87ac45..400a18a8103b9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -15,14 +15,14 @@ import type { ActionType, AsApiContract } from '@kbn/actions-plugin/common'; import type { ActionResult } from '@kbn/actions-plugin/server'; import { convertRulesFilterToKQL } from '../../../../common/detection_engine/rule_management/rule_filtering'; import type { - UpgradeSpecificRulesRequest, - PickVersionValues, PerformRuleUpgradeResponseBody, InstallSpecificRulesRequest, PerformRuleInstallationResponseBody, GetPrebuiltRulesStatusResponseBody, ReviewRuleUpgradeResponseBody, ReviewRuleInstallationResponseBody, + ReviewRuleUpgradeRequestBody, + PerformRuleUpgradeRequestBody, } from '../../../../common/api/detection_engine/prebuilt_rules'; import type { BulkDuplicateRules, @@ -637,13 +637,16 @@ export const getPrebuiltRulesStatus = async ({ */ export const reviewRuleUpgrade = async ({ signal, + request, }: { signal: AbortSignal | undefined; + request: ReviewRuleUpgradeRequestBody; }): Promise => KibanaServices.get().http.fetch(REVIEW_RULE_UPGRADE_URL, { method: 'POST', version: '1', signal, + body: JSON.stringify(request), }); /** @@ -685,23 +688,13 @@ export const performInstallSpecificRules = async ( }), }); -export interface PerformUpgradeRequest { - rules: UpgradeSpecificRulesRequest['rules']; - pickVersion: PickVersionValues; -} - -export const performUpgradeSpecificRules = async ({ - rules, - pickVersion, -}: PerformUpgradeRequest): Promise => +export const performUpgradeRules = async ( + body: PerformRuleUpgradeRequestBody +): Promise => KibanaServices.get().http.fetch(PERFORM_RULE_UPGRADE_URL, { method: 'POST', version: '1', - body: JSON.stringify({ - mode: 'SPECIFIC_RULES', - rules, - pick_version: pickVersion, - }), + body: JSON.stringify(body), }); export const bootstrapPrebuiltRules = async (): Promise => diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query.ts index 0c0515e61b818..376877326a5e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query.ts @@ -4,24 +4,24 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useCallback } from 'react'; import type { UseQueryOptions } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import type { PrebuiltRulesStatusStats } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { useCallback } from 'react'; +import type { GetPrebuiltRulesStatusResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { GET_PREBUILT_RULES_STATUS_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { getPrebuiltRulesStatus } from '../../api'; import { DEFAULT_QUERY_OPTIONS } from '../constants'; -import { GET_PREBUILT_RULES_STATUS_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules'; export const PREBUILT_RULES_STATUS_QUERY_KEY = ['GET', GET_PREBUILT_RULES_STATUS_URL]; export const useFetchPrebuiltRulesStatusQuery = ( - options?: UseQueryOptions + options?: UseQueryOptions ) => { - return useQuery( + return useQuery( PREBUILT_RULES_STATUS_QUERY_KEY, async ({ signal }) => { const response = await getPrebuiltRulesStatus({ signal }); - return response.stats; + return response; }, { ...DEFAULT_QUERY_OPTIONS, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts index 532114b1d4b62..4b779918febc3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_upgrade_review_query.ts @@ -9,7 +9,10 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { reviewRuleUpgrade } from '../../api'; import { REVIEW_RULE_UPGRADE_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules/urls'; -import type { ReviewRuleUpgradeResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import type { + ReviewRuleUpgradeRequestBody, + ReviewRuleUpgradeResponseBody, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { DEFAULT_QUERY_OPTIONS } from '../constants'; import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; import { cappedExponentialBackoff } from './capped_exponential_backoff'; @@ -17,12 +20,13 @@ import { cappedExponentialBackoff } from './capped_exponential_backoff'; export const REVIEW_RULE_UPGRADE_QUERY_KEY = ['POST', REVIEW_RULE_UPGRADE_URL]; export const useFetchPrebuiltRulesUpgradeReviewQuery = ( + request: ReviewRuleUpgradeRequestBody, options?: UseQueryOptions ) => { return useQuery( - REVIEW_RULE_UPGRADE_QUERY_KEY, + [...REVIEW_RULE_UPGRADE_QUERY_KEY, request], async ({ signal }) => { - const response = await reviewRuleUpgrade({ signal }); + const response = await reviewRuleUpgrade({ signal, request }); return response; }, { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_rules_upgrade_mutation.ts similarity index 79% rename from x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts rename to x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_rules_upgrade_mutation.ts index 84b074449603d..ecb604d63d4e0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/api/hooks/prebuilt_rules/use_perform_rules_upgrade_mutation.ts @@ -6,10 +6,12 @@ */ import type { UseMutationOptions } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query'; -import type { PerformRuleUpgradeResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import type { + PerformRuleUpgradeRequestBody, + PerformRuleUpgradeResponseBody, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { PERFORM_RULE_UPGRADE_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules/urls'; -import type { PerformUpgradeRequest } from '../../api'; -import { performUpgradeSpecificRules } from '../../api'; +import { performUpgradeRules } from '../../api'; import { useInvalidateFetchCoverageOverviewQuery } from '../use_fetch_coverage_overview_query'; import { useInvalidateFetchRuleManagementFiltersQuery } from '../use_fetch_rule_management_filters_query'; import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../use_fetch_rules_snooze_settings_query'; @@ -19,14 +21,14 @@ import { useInvalidateFetchPrebuiltRulesUpgradeReviewQuery } from './use_fetch_p import { retryOnRateLimitedError } from './retry_on_rate_limited_error'; import { cappedExponentialBackoff } from './capped_exponential_backoff'; -export const PERFORM_SPECIFIC_RULES_UPGRADE_KEY = [ - 'POST', - 'SPECIFIC_RULES', - PERFORM_RULE_UPGRADE_URL, -]; +export const PERFORM_RULES_UPGRADE_KEY = ['POST', PERFORM_RULE_UPGRADE_URL]; -export const usePerformSpecificRulesUpgradeMutation = ( - options?: UseMutationOptions +export const usePerformRulesUpgradeMutation = ( + options?: UseMutationOptions< + PerformRuleUpgradeResponseBody, + unknown, + PerformRuleUpgradeRequestBody + > ) => { const invalidateFindRulesQuery = useInvalidateFindRulesQuery(); const invalidateFetchRulesSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery(); @@ -37,13 +39,13 @@ export const usePerformSpecificRulesUpgradeMutation = ( const invalidateRuleStatus = useInvalidateFetchPrebuiltRulesStatusQuery(); const invalidateFetchCoverageOverviewQuery = useInvalidateFetchCoverageOverviewQuery(); - return useMutation( - (args: PerformUpgradeRequest) => { - return performUpgradeSpecificRules(args); + return useMutation( + (args: PerformRuleUpgradeRequestBody) => { + return performUpgradeRules(args); }, { ...options, - mutationKey: PERFORM_SPECIFIC_RULES_UPGRADE_KEY, + mutationKey: PERFORM_RULES_UPGRADE_KEY, onSettled: (...args) => { invalidatePrePackagedRulesStatus(); invalidateFindRulesQuery(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts index 33f36ffe14da2..e1ae2768dacc6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_perform_rule_upgrade.ts @@ -5,18 +5,22 @@ * 2.0. */ import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { usePerformSpecificRulesUpgradeMutation } from '../../api/hooks/prebuilt_rules/use_perform_specific_rules_upgrade_mutation'; +import { usePerformRulesUpgradeMutation } from '../../api/hooks/prebuilt_rules/use_perform_rules_upgrade_mutation'; import * as i18n from './translations'; -export const usePerformUpgradeSpecificRules = () => { +export const usePerformUpgradeRules = () => { const { addError, addSuccess } = useAppToasts(); - return usePerformSpecificRulesUpgradeMutation({ + return usePerformRulesUpgradeMutation({ onError: (err) => { addError(err, { title: i18n.RULE_UPGRADE_FAILED }); }, - onSuccess: (result) => { + onSuccess: (result, vars) => { + if (vars.dry_run) { + // This is a preflight check, no need to show toast + return; + } addSuccess(getSuccessToastMessage(result)); }, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review.ts index 6e8f008c5ede5..bb0b04174d0dd 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review.ts @@ -5,7 +5,10 @@ * 2.0. */ import type { UseQueryOptions } from '@tanstack/react-query'; -import type { ReviewRuleUpgradeResponseBody } from '../../../../../common/api/detection_engine/prebuilt_rules'; +import type { + ReviewRuleUpgradeRequestBody, + ReviewRuleUpgradeResponseBody, +} from '../../../../../common/api/detection_engine/prebuilt_rules'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import * as i18n from '../translations'; @@ -18,11 +21,12 @@ import { useFetchPrebuiltRulesUpgradeReviewQuery } from '../../api/hooks/prebuil * @returns useQuery result */ export const usePrebuiltRulesUpgradeReview = ( + request: ReviewRuleUpgradeRequestBody, options?: UseQueryOptions ) => { const { addError } = useAppToasts(); - return useFetchPrebuiltRulesUpgradeReviewQuery({ + return useFetchPrebuiltRulesUpgradeReviewQuery(request, { onError: (error) => addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }), ...options, }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts index e91639dd0454c..98383e93b444c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts @@ -11,7 +11,10 @@ import type { RuleSnooze } from '@kbn/alerting-plugin/common'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import type { RuleSnoozeSettings } from '@kbn/triggers-actions-ui-plugin/public/types'; -import type { WarningSchema } from '../../../../common/api/detection_engine'; +import type { + RuleCustomizationStatus, + WarningSchema, +} from '../../../../common/api/detection_engine'; import type { RuleExecutionStatus } from '../../../../common/api/detection_engine/rule_monitoring'; import { SortOrder } from '../../../../common/api/detection_engine'; @@ -103,7 +106,7 @@ export interface FilterOptions { excludeRuleTypes?: Type[]; enabled?: boolean; // undefined is to display all the rules ruleExecutionStatus?: RuleExecutionStatus; // undefined means "all" - ruleSource?: RuleCustomizationEnum[]; // undefined is to display all the rules + ruleSource?: RuleCustomizationStatus[]; // undefined is to display all the rules showRulesWithGaps?: boolean; gapSearchRange?: GapRangeValue; } @@ -209,8 +212,3 @@ export interface FindRulesReferencedByExceptionsProps { lists: FindRulesReferencedByExceptionsListProp[]; signal?: AbortSignal; } - -export enum RuleCustomizationEnum { - customized = 'CUSTOMIZED', - not_customized = 'NOT_CUSTOMIZED', -} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx index c4eacb4a01ff5..6ca59501dbd7a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rule_update_callouts/rule_update_callouts.tsx @@ -21,8 +21,8 @@ import { AllRulesTabs } from '../rules_table/rules_table_toolbar'; export const RuleUpdateCallouts = () => { const { data: prebuiltRulesStatus } = usePrebuiltRulesStatus(); - const rulesToInstallCount = prebuiltRulesStatus?.num_prebuilt_rules_to_install ?? 0; - const rulesToUpgradeCount = prebuiltRulesStatus?.num_prebuilt_rules_to_upgrade ?? 0; + const rulesToInstallCount = prebuiltRulesStatus?.stats.num_prebuilt_rules_to_install ?? 0; + const rulesToUpgradeCount = prebuiltRulesStatus?.stats.num_prebuilt_rules_to_upgrade ?? 0; // Check against rulesInstalledCount since we don't want to show banners if we're showing the empty prompt const shouldDisplayNewRulesCallout = rulesToInstallCount > 0; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx index bb949ba436995..59d66a402dc88 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx @@ -144,7 +144,7 @@ export const AddPrebuiltRulesTableContextProvider = ({ enabled: isUpgradeReviewRequestEnabled({ canUserCRUD, isUpgradingSecurityPackages, - prebuiltRulesStatus, + prebuiltRulesStatus: prebuiltRulesStatus?.stats, }), }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx index e3618a1383598..eb9d17ebfbe9c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx @@ -33,7 +33,7 @@ export const RulesTableToolbar = React.memo(() => { const installedTotal = (ruleManagementFilters?.rules_summary.custom_count ?? 0) + (ruleManagementFilters?.rules_summary.prebuilt_installed_count ?? 0); - const updateTotal = prebuiltRulesStatus?.num_prebuilt_rules_to_upgrade ?? 0; + const updateTotal = prebuiltRulesStatus?.stats.num_prebuilt_rules_to_upgrade ?? 0; const shouldDisplayRuleUpdatesTab = !loading && canUserCRUD && updateTotal > 0; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.tsx index 73fb4e9381d73..a5b2754e939ad 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations.tsx @@ -34,13 +34,6 @@ export const BULK_UPDATE_BUTTON_TOOLTIP_NO_PERMISSIONS = i18n.translate( } ); -export const BULK_UPDATE_ALL_RULES_BUTTON_TOOLTIP_CONFLICTS = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.upgradeRules.bulkButtons.allRules.conflicts', - { - defaultMessage: 'All rules have conflicts. Update them individually.', - } -); - export const BULK_UPDATE_SELECTED_RULES_BUTTON_TOOLTIP_CONFLICTS = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.upgradeRules.bulkButtons.selectedRules.conflicts', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx index dfe8c5787417c..959f20ef72c20 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table.tsx @@ -10,7 +10,7 @@ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, - EuiInMemoryTable, + EuiBasicTable, EuiProgress, EuiSkeletonLoading, EuiSkeletonText, @@ -19,9 +19,10 @@ import { import React, { useCallback, useState } from 'react'; import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade'; import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations'; -import { RULES_TABLE_INITIAL_PAGE_SIZE, RULES_TABLE_PAGE_SIZE_OPTIONS } from '../constants'; +import { RULES_TABLE_PAGE_SIZE_OPTIONS } from '../constants'; import { RulesChangelogLink } from '../rules_changelog_link'; import { UpgradePrebuiltRulesTableButtons } from './upgrade_prebuilt_rules_table_buttons'; +import type { UpgradePrebuiltRulesSortingOptions } from './upgrade_prebuilt_rules_table_context'; import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context'; import { UpgradePrebuiltRulesTableFilters } from './upgrade_prebuilt_rules_table_filters'; import { useUpgradePrebuiltRulesTableColumns } from './use_upgrade_prebuilt_rules_table_columns'; @@ -44,20 +45,32 @@ export const UpgradePrebuiltRulesTable = React.memo(() => { ruleUpgradeStates, hasRulesToUpgrade, isLoading, + isFetching, isRefetching, isUpgradingSecurityPackages, + pagination, + sortingOptions, }, + actions: { setPagination, setSortingOptions }, } = useUpgradePrebuiltRulesTableContext(); const [selected, setSelected] = useState([]); const rulesColumns = useUpgradePrebuiltRulesTableColumns(); const shouldShowProgress = isUpgradingSecurityPackages || isRefetching; - const [pageIndex, setPageIndex] = useState(0); const handleTableChange = useCallback( - ({ page: { index } }: CriteriaWithPagination) => { - setPageIndex(index); + ({ page: { index, size }, sort }: CriteriaWithPagination) => { + setPagination({ + page: index + 1, + perPage: size, + }); + if (sort) { + setSortingOptions({ + field: sort.field as UpgradePrebuiltRulesSortingOptions['field'], + order: sort.direction, + }); + } }, - [setPageIndex] + [setPagination, setSortingOptions] ); return ( @@ -104,23 +117,31 @@ export const UpgradePrebuiltRulesTable = React.memo(() => { - true, onSelectionChange: setSelected, initialSelected: selected, }} + sorting={{ + sort: { + // EuiBasicTable has incorrect `sort.field` types which accept only `keyof Item` and reject fields in dot notation + field: sortingOptions.field as keyof RuleUpgradeState, + direction: sortingOptions.order, + }, + }} itemId="rule_id" data-test-subj="rules-upgrades-table" columns={rulesColumns} - onTableChange={handleTableChange} + onChange={handleTableChange} /> ) diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx index 37e189f5e4b79..5498e8961c06a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_buttons.tsx @@ -21,13 +21,7 @@ export const UpgradePrebuiltRulesTableButtons = ({ selectedRules, }: UpgradePrebuiltRulesTableButtonsProps) => { const { - state: { - ruleUpgradeStates, - hasRulesToUpgrade, - loadingRules, - isRefetching, - isUpgradingSecurityPackages, - }, + state: { hasRulesToUpgrade, loadingRules, isRefetching, isUpgradingSecurityPackages }, actions: { upgradeRules, upgradeAllRules }, } = useUpgradePrebuiltRulesTableContext(); const { isRulesCustomizationEnabled } = usePrebuiltRulesCustomizationStatus(); @@ -43,14 +37,10 @@ export const UpgradePrebuiltRulesTableButtons = ({ const doAllSelectedRulesHaveConflicts = isRulesCustomizationEnabled && selectedRules.every(({ hasUnresolvedConflicts }) => hasUnresolvedConflicts); - const doAllRulesHaveConflicts = - isRulesCustomizationEnabled && - ruleUpgradeStates.every(({ hasUnresolvedConflicts }) => hasUnresolvedConflicts); const { selectedRulesButtonTooltip, allRulesButtonTooltip } = useBulkUpdateButtonsTooltipContent({ canUserEditRules, doAllSelectedRulesHaveConflicts, - doAllRulesHaveConflicts, isPrebuiltRulesCustomizationEnabled: isRulesCustomizationEnabled, }); @@ -83,12 +73,7 @@ export const UpgradePrebuiltRulesTableButtons = ({ fill iconType="plusInCircle" onClick={upgradeAllRules} - disabled={ - !canUserEditRules || - !hasRulesToUpgrade || - isRequestInProgress || - doAllRulesHaveConflicts - } + disabled={!canUserEditRules || !hasRulesToUpgrade || isRequestInProgress} data-test-subj="upgradeAllRulesButton" > {i18n.UPDATE_ALL} @@ -103,12 +88,10 @@ export const UpgradePrebuiltRulesTableButtons = ({ const useBulkUpdateButtonsTooltipContent = ({ canUserEditRules, doAllSelectedRulesHaveConflicts, - doAllRulesHaveConflicts, isPrebuiltRulesCustomizationEnabled, }: { canUserEditRules: boolean | null; doAllSelectedRulesHaveConflicts: boolean; - doAllRulesHaveConflicts: boolean; isPrebuiltRulesCustomizationEnabled: boolean; }) => { if (!canUserEditRules) { @@ -125,13 +108,6 @@ const useBulkUpdateButtonsTooltipContent = ({ }; } - if (doAllRulesHaveConflicts) { - return { - selectedRulesButtonTooltip: i18n.BULK_UPDATE_SELECTED_RULES_BUTTON_TOOLTIP_CONFLICTS, - allRulesButtonTooltip: i18n.BULK_UPDATE_ALL_RULES_BUTTON_TOOLTIP_CONFLICTS, - }; - } - if (doAllSelectedRulesHaveConflicts) { return { selectedRulesButtonTooltip: i18n.BULK_UPDATE_SELECTED_RULES_BUTTON_TOOLTIP_CONFLICTS, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx index 3be956c7cbf94..b0c7f93234c99 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx @@ -10,8 +10,11 @@ import type { Dispatch, SetStateAction } from 'react'; import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { PrebuiltRulesCustomizationDisabledReason } from '../../../../../../common/detection_engine/prebuilt_rules/prebuilt_rule_customization_status'; import type { + FindRulesSortField, + PrebuiltRulesFilter, RuleFieldsToUpgrade, RuleUpgradeSpecifier, + SortOrder, } from '../../../../../../common/api/detection_engine'; import { usePrebuiltRulesCustomizationStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status'; import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade'; @@ -24,13 +27,11 @@ import type { } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { invariant } from '../../../../../../common/utils/invariant'; import { TabContentPadding } from '../../../../rule_management/components/rule_details/rule_details_flyout'; -import { usePerformUpgradeSpecificRules } from '../../../../rule_management/logic/prebuilt_rules/use_perform_rule_upgrade'; +import { usePerformUpgradeRules } from '../../../../rule_management/logic/prebuilt_rules/use_perform_rule_upgrade'; import { usePrebuiltRulesUpgradeReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_upgrade_review'; import { RuleDiffTab } from '../../../../rule_management/components/rule_details/rule_diff_tab'; import { FieldUpgradeStateEnum } from '../../../../rule_management/model/prebuilt_rule_upgrade/field_upgrade_state_enum'; import { useRulePreviewFlyout } from '../use_rule_preview_flyout'; -import type { UpgradePrebuiltRulesTableFilterOptions } from './use_filter_prebuilt_rules_to_upgrade'; -import { useFilterPrebuiltRulesToUpgrade } from './use_filter_prebuilt_rules_to_upgrade'; import { usePrebuiltRulesUpgradeState } from './use_prebuilt_rules_upgrade_state'; import { useOutdatedMlJobsUpgradeModal } from './use_ml_jobs_upgrade_modal'; import { useUpgradeWithConflictsModal } from './use_upgrade_with_conflicts_modal'; @@ -39,9 +40,21 @@ import { UpgradeFlyoutSubHeader } from './upgrade_flyout_subheader'; import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations'; import * as i18n from './translations'; import { CustomizationDisabledCallout } from './customization_disabled_callout'; +import { RULES_TABLE_INITIAL_PAGE_SIZE } from '../constants'; +import type { PaginationOptions } from '../../../../rule_management/logic'; +import { usePrebuiltRulesStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_status'; const REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL = 5 * 60 * 1000; +export interface UpgradePrebuiltRulesSortingOptions { + field: + | 'current_rule.name' + | 'current_rule.risk_score' + | 'current_rule.severity' + | 'current_rule.last_updated'; + order: SortOrder; +} + export interface UpgradePrebuiltRulesTableState { /** * Rule upgrade state after applying `filterOptions` @@ -50,7 +63,7 @@ export interface UpgradePrebuiltRulesTableState { /** * Currently selected table filter */ - filterOptions: UpgradePrebuiltRulesTableFilterOptions; + filterOptions: PrebuiltRulesFilter; /** * All unique tags for all rules */ @@ -63,6 +76,10 @@ export interface UpgradePrebuiltRulesTableState { * Is true then there is no cached data and the query is currently fetching. */ isLoading: boolean; + /** + * Is true whenever a request is in-flight, which includes initial loading as well as background refetches. + */ + isFetching: boolean; /** * Will be true if the query has been fetched. */ @@ -84,6 +101,14 @@ export interface UpgradePrebuiltRulesTableState { * The timestamp for when the rules were successfully fetched */ lastUpdated: number; + /** + * Current pagination state + */ + pagination: PaginationOptions; + /** + * Currently selected table sorting + */ + sortingOptions: UpgradePrebuiltRulesSortingOptions; } export const PREBUILT_RULE_UPDATE_FLYOUT_ANCHOR = 'updatePrebuiltRulePreview'; @@ -92,7 +117,9 @@ export interface UpgradePrebuiltRulesTableActions { reFetchRules: () => void; upgradeRules: (ruleIds: RuleSignatureId[]) => void; upgradeAllRules: () => void; - setFilterOptions: Dispatch>; + setFilterOptions: Dispatch>; + setPagination: Dispatch>; + setSortingOptions: Dispatch>; openRulePreview: (ruleId: string) => void; } @@ -121,35 +148,71 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ }: UpgradePrebuiltRulesTableContextProviderProps) => { const { isRulesCustomizationEnabled, customizationDisabledReason } = usePrebuiltRulesCustomizationStatus(); + + // Use the data from the prebuilt rules status API to determine if there are + // rules to upgrade because it returns information about all rules without filters + const { data: prebuiltRulesStatusResponse } = usePrebuiltRulesStatus(); + const hasRulesToUpgrade = + (prebuiltRulesStatusResponse?.stats.num_prebuilt_rules_to_upgrade ?? 0) > 0; + const tags = prebuiltRulesStatusResponse?.aggregated_fields?.upgradeable_rules.tags; + const [loadingRules, setLoadingRules] = useState([]); - const [filterOptions, setFilterOptions] = useState({ - filter: '', - tags: [], - ruleSource: [], - }); + const [filterOptions, setFilterOptions] = useState({}); const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages(); + const [pagination, setPagination] = useState({ + page: 1, + perPage: RULES_TABLE_INITIAL_PAGE_SIZE, + }); + const [sortingOptions, setSortingOptions] = useState({ + field: 'current_rule.last_updated', + order: 'asc', + }); + + const findRulesSortField = useMemo( + () => + (( + { + 'current_rule.name': 'name', + 'current_rule.risk_score': 'risk_score', + 'current_rule.severity': 'severity', + 'current_rule.last_updated': 'updated_at', + } as const + )[sortingOptions.field]), + [sortingOptions.field] + ); const { - data: { rules: ruleUpgradeInfos, stats: { tags } } = { - rules: [], - stats: { tags: [] }, - }, + data: upgradeReviewResponse, refetch, dataUpdatedAt, isFetched, isLoading, + isFetching, isRefetching, - } = usePrebuiltRulesUpgradeReview({ - refetchInterval: REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL, - keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change - }); + } = usePrebuiltRulesUpgradeReview( + { + page: pagination.page, + per_page: pagination.perPage, + sort: { + field: findRulesSortField, + order: sortingOptions.order, + }, + filter: filterOptions, + }, + { + refetchInterval: REVIEW_PREBUILT_RULES_UPGRADE_REFRESH_INTERVAL, + keepPreviousData: true, // Use this option so that the state doesn't jump between "success" and "loading" on page change + } + ); + + const upgradeableRules = useMemo( + () => upgradeReviewResponse?.rules ?? [], + [upgradeReviewResponse] + ); + const { rulesUpgradeState, setRuleFieldResolvedValue } = - usePrebuiltRulesUpgradeState(ruleUpgradeInfos); + usePrebuiltRulesUpgradeState(upgradeableRules); const ruleUpgradeStates = useMemo(() => Object.values(rulesUpgradeState), [rulesUpgradeState]); - const filteredRuleUpgradeStates = useFilterPrebuiltRulesToUpgrade({ - filterOptions, - data: ruleUpgradeStates, - }); const { modal: confirmLegacyMlJobsUpgradeModal, @@ -158,7 +221,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ } = useOutdatedMlJobsUpgradeModal(); const { modal: upgradeConflictsModal, confirmConflictsUpgrade } = useUpgradeWithConflictsModal(); - const { mutateAsync: upgradeSpecificRulesRequest } = usePerformUpgradeSpecificRules(); + const { mutateAsync: upgradeRulesRequest } = usePerformUpgradeRules(); const upgradeRulesToResolved = useCallback( async (ruleIds: RuleSignatureId[]) => { @@ -189,8 +252,9 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ return; } - await upgradeSpecificRulesRequest({ - pickVersion: 'MERGED', + await upgradeRulesRequest({ + mode: 'SPECIFIC_RULES', + pick_version: 'MERGED', rules: ruleUpgradeSpecifiers, }); } catch { @@ -201,7 +265,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id))); } }, - [confirmLegacyMLJobs, confirmConflictsUpgrade, rulesUpgradeState, upgradeSpecificRulesRequest] + [confirmLegacyMLJobs, confirmConflictsUpgrade, rulesUpgradeState, upgradeRulesRequest] ); const upgradeRulesToTarget = useCallback( @@ -220,8 +284,9 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ return; } - await upgradeSpecificRulesRequest({ - pickVersion: 'TARGET', + await upgradeRulesRequest({ + mode: 'SPECIFIC_RULES', + pick_version: 'TARGET', rules: ruleUpgradeSpecifiers, }); } catch { @@ -232,7 +297,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ setLoadingRules((prev) => prev.filter((id) => !upgradedRuleIdsSet.has(id))); } }, - [confirmLegacyMLJobs, rulesUpgradeState, upgradeSpecificRulesRequest] + [confirmLegacyMLJobs, rulesUpgradeState, upgradeRulesRequest] ); const upgradeRules = useCallback( @@ -246,11 +311,50 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ [isRulesCustomizationEnabled, upgradeRulesToResolved, upgradeRulesToTarget] ); - const upgradeAllRules = useCallback( - // Upgrade all rules, ignoring filter and selection - () => upgradeRules(ruleUpgradeInfos.map((rule) => rule.rule_id)), - [ruleUpgradeInfos, upgradeRules] - ); + const upgradeAllRules = useCallback(async () => { + setLoadingRules((prev) => [...prev, ...upgradeableRules.map((rule) => rule.rule_id)]); + + try { + // Handle MLJobs modal + if (!(await confirmLegacyMLJobs())) { + return; + } + + const dryRunResults = await upgradeRulesRequest({ + mode: 'ALL_RULES', + pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET', + filter: filterOptions, + dry_run: true, + on_conflict: 'SKIP', + }); + + const hasConflicts = dryRunResults.results.skipped.some( + (skippedRule) => skippedRule.reason === 'CONFLICT' + ); + + if (hasConflicts && !(await confirmConflictsUpgrade())) { + return; + } + + await upgradeRulesRequest({ + mode: 'ALL_RULES', + pick_version: isRulesCustomizationEnabled ? 'MERGED' : 'TARGET', + filter: filterOptions, + on_conflict: 'SKIP', + }); + } catch { + // Error is handled by the mutation's onError callback, so no need to do anything here + } finally { + setLoadingRules([]); + } + }, [ + upgradeableRules, + confirmLegacyMLJobs, + upgradeRulesRequest, + isRulesCustomizationEnabled, + filterOptions, + confirmConflictsUpgrade, + ]); const subHeaderFactory = useCallback( (rule: RuleResponse) => @@ -377,12 +481,8 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ setRuleFieldResolvedValue, ] ); - const filteredRules = useMemo( - () => filteredRuleUpgradeStates.map(({ target_rule: targetRule }) => targetRule), - [filteredRuleUpgradeStates] - ); const { rulePreviewFlyout, openRulePreview } = useRulePreviewFlyout({ - rules: filteredRules, + rules: ruleUpgradeStates.map(({ target_rule: targetRule }) => targetRule), subHeaderFactory, ruleActionsFactory, extraTabsFactory, @@ -399,6 +499,8 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ upgradeAllRules, setFilterOptions, openRulePreview, + setPagination, + setSortingOptions, }), [refetch, upgradeRules, upgradeAllRules, openRulePreview] ); @@ -406,31 +508,41 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ const providerValue = useMemo( () => ({ state: { - ruleUpgradeStates: filteredRuleUpgradeStates, - hasRulesToUpgrade: isFetched && ruleUpgradeInfos.length > 0, + ruleUpgradeStates, + hasRulesToUpgrade, filterOptions, - tags, + tags: tags ?? [], isFetched, isLoading: isLoading || areMlJobsLoading, + isFetching, isRefetching, isUpgradingSecurityPackages, loadingRules, lastUpdated: dataUpdatedAt, + pagination: { + ...pagination, + total: upgradeReviewResponse?.total ?? 0, + }, + sortingOptions, }, actions, }), [ - ruleUpgradeInfos.length, - filteredRuleUpgradeStates, + ruleUpgradeStates, + hasRulesToUpgrade, filterOptions, tags, isFetched, isLoading, areMlJobsLoading, + isFetching, isRefetching, isUpgradingSecurityPackages, loadingRules, dataUpdatedAt, + pagination, + upgradeReviewResponse?.total, + sortingOptions, actions, ] ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx index d8b8f618cf43d..b62f987b8abe2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_filters.tsx @@ -9,11 +9,11 @@ import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEqual } from 'lodash/fp'; import React, { useCallback } from 'react'; import styled from 'styled-components'; +import type { RuleCustomizationStatus } from '../../../../../../common/api/detection_engine'; import { usePrebuiltRulesCustomizationStatus } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_customization_status'; -import type { RuleCustomizationEnum } from '../../../../rule_management/logic'; -import * as i18n from './translations'; -import { TagsFilterPopover } from '../rules_table_filters/tags_filter_popover'; import { RuleSearchField } from '../rules_table_filters/rule_search_field'; +import { TagsFilterPopover } from '../rules_table_filters/tags_filter_popover'; +import * as i18n from './translations'; import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context'; import { RuleCustomizationFilterPopover } from './upgrade_rule_customization_filter_popover'; @@ -33,13 +33,13 @@ const UpgradePrebuiltRulesTableFiltersComponent = () => { const { isRulesCustomizationEnabled } = usePrebuiltRulesCustomizationStatus(); - const { tags: selectedTags, ruleSource: selectedRuleSource = [] } = filterOptions; + const { tags: selectedTags, customization_status: customizationStatus } = filterOptions; const handleOnSearch = useCallback( - (filterString: string) => { + (nameString: string) => { setFilterOptions((filters) => ({ ...filters, - filter: filterString.trim(), + name: nameString.trim(), })); }, [setFilterOptions] @@ -57,22 +57,20 @@ const UpgradePrebuiltRulesTableFiltersComponent = () => { [selectedTags, setFilterOptions] ); - const handleSelectedRuleSource = useCallback( - (newRuleSource: RuleCustomizationEnum[]) => { - if (!isEqual(newRuleSource, selectedRuleSource)) { - setFilterOptions((filters) => ({ - ...filters, - ruleSource: newRuleSource, - })); - } + const handleCustomizationStatusChange = useCallback( + (newCustomizationStatus: RuleCustomizationStatus | undefined) => { + setFilterOptions((filters) => ({ + ...filters, + customization_status: newCustomizationStatus, + })); }, - [selectedRuleSource, setFilterOptions] + [setFilterOptions] ); return ( @@ -81,8 +79,8 @@ const UpgradePrebuiltRulesTableFiltersComponent = () => { {isRulesCustomizationEnabled && ( @@ -90,7 +88,7 @@ const UpgradePrebuiltRulesTableFiltersComponent = () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx index 234943e333272..05e68f4ebbccb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_rule_customization_filter_popover.tsx @@ -5,23 +5,22 @@ * 2.0. */ -import React, { useState, useMemo } from 'react'; import type { EuiSelectableOption } from '@elastic/eui'; import { EuiFilterButton, EuiPopover, EuiSelectable } from '@elastic/eui'; -import { RuleCustomizationEnum } from '../../../../rule_management/logic'; +import React, { useMemo, useState } from 'react'; +import { RuleCustomizationStatus } from '../../../../../../common/api/detection_engine'; import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations'; -import { toggleSelectedGroup } from '../../../../../common/components/ml_popover/jobs_table/filters/toggle_selected_group'; interface RuleCustomizationFilterPopoverProps { - selectedRuleSource: RuleCustomizationEnum[]; - onSelectedRuleSourceChanged: (newRuleSource: RuleCustomizationEnum[]) => void; + customizationStatus: RuleCustomizationStatus | undefined; + onCustomizationStatusChanged: (newRuleSource: RuleCustomizationStatus | undefined) => void; } const RULE_CUSTOMIZATION_POPOVER_WIDTH = 200; const RuleCustomizationFilterPopoverComponent = ({ - selectedRuleSource, - onSelectedRuleSourceChanged, + customizationStatus, + onCustomizationStatusChanged, }: RuleCustomizationFilterPopoverProps) => { const [isRuleCustomizationPopoverOpen, setIsRuleCustomizationPopoverOpen] = useState(false); @@ -29,18 +28,16 @@ const RuleCustomizationFilterPopoverComponent = ({ () => [ { label: i18n.MODIFIED_LABEL, - key: RuleCustomizationEnum.customized, - checked: selectedRuleSource.includes(RuleCustomizationEnum.customized) ? 'on' : undefined, + key: RuleCustomizationStatus.CUSTOMIZED, + checked: customizationStatus === RuleCustomizationStatus.CUSTOMIZED ? 'on' : undefined, }, { label: i18n.UNMODIFIED_LABEL, - key: RuleCustomizationEnum.not_customized, - checked: selectedRuleSource.includes(RuleCustomizationEnum.not_customized) - ? 'on' - : undefined, + key: RuleCustomizationStatus.NOT_CUSTOMIZED, + checked: customizationStatus === RuleCustomizationStatus.NOT_CUSTOMIZED ? 'on' : undefined, }, ], - [selectedRuleSource] + [customizationStatus] ); const handleSelectableOptionsChange = ( @@ -48,10 +45,8 @@ const RuleCustomizationFilterPopoverComponent = ({ _: unknown, changedOption: EuiSelectableOption ) => { - toggleSelectedGroup( - changedOption.key ?? '', - selectedRuleSource, - onSelectedRuleSourceChanged as (args: string[]) => void + onCustomizationStatusChanged( + changedOption.checked === 'on' ? (changedOption.key as RuleCustomizationStatus) : undefined ); }; @@ -62,8 +57,8 @@ const RuleCustomizationFilterPopoverComponent = ({ onClick={() => setIsRuleCustomizationPopoverOpen(!isRuleCustomizationPopoverOpen)} numFilters={selectableOptions.length} isSelected={isRuleCustomizationPopoverOpen} - hasActiveFilters={selectedRuleSource.length > 0} - numActiveFilters={selectedRuleSource.length} + hasActiveFilters={customizationStatus != null} + numActiveFilters={customizationStatus != null ? 1 : 0} data-test-subj="rule-customization-filter-popover-button" > {i18n.RULE_SOURCE} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts deleted file mode 100644 index f0a818fd2532e..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_filter_prebuilt_rules_to_upgrade.ts +++ /dev/null @@ -1,48 +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 { useMemo } from 'react'; -import type { RuleUpgradeState } from '../../../../rule_management/model/prebuilt_rule_upgrade'; -import { RuleCustomizationEnum, type FilterOptions } from '../../../../rule_management/logic/types'; - -export type UpgradePrebuiltRulesTableFilterOptions = Pick< - FilterOptions, - 'filter' | 'tags' | 'ruleSource' ->; - -interface UseFilterPrebuiltRulesToUpgradeParams { - data: RuleUpgradeState[]; - filterOptions: UpgradePrebuiltRulesTableFilterOptions; -} - -export const useFilterPrebuiltRulesToUpgrade = ({ - data, - filterOptions, -}: UseFilterPrebuiltRulesToUpgradeParams): RuleUpgradeState[] => { - return useMemo(() => { - const { filter, tags, ruleSource } = filterOptions; - - return data.filter((ruleInfo) => { - if (filter && !ruleInfo.current_rule.name.toLowerCase().includes(filter.toLowerCase())) { - return false; - } - - if (tags?.length && !tags.every((tag) => ruleInfo.current_rule.tags.includes(tag))) { - return false; - } - - if (ruleSource?.length === 1 && ruleInfo.current_rule.rule_source.type === 'external') { - if (ruleSource.includes(RuleCustomizationEnum.customized)) { - return ruleInfo.current_rule.rule_source.is_customized; - } - return ruleInfo.current_rule.rule_source.is_customized === false; - } - - return true; - }); - }, [filterOptions, data]); -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts index 28b24a4efe9a5..3a35e783975e9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.test.ts @@ -429,6 +429,7 @@ function createRuleUpgradeInfoMock( num_fields_with_non_solvable_conflicts: 0, fields: {}, }, + version: 1, revision: 1, ...rewrites, }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/add_elastic_rules_button.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/add_elastic_rules_button.tsx index d62c080ace66f..e0f87a3ee22cc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/add_elastic_rules_button.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/add_elastic_rules_button.tsx @@ -33,7 +33,7 @@ export const AddElasticRulesButton = ({ }); const { data: preBuiltRulesStatus } = usePrebuiltRulesStatus(); - const newRulesCount = preBuiltRulesStatus?.num_prebuilt_rules_to_install ?? 0; + const newRulesCount = preBuiltRulesStatus?.stats.num_prebuilt_rules_to_install ?? 0; const ButtonComponent = fill ? EuiButton : EuiButtonEmpty; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts index 52e2c552c74fa..adccc7ea6488d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts @@ -12,8 +12,6 @@ import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { buildSiemResponse } from '../../../routes/utils'; import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; -import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; -import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter) => { router.versioned @@ -41,19 +39,33 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); - const ruleVersionsMap = await fetchRuleVersionsTriad({ - ruleAssetsClient, - ruleObjectsClient, + const currentRuleVersions = await ruleObjectsClient.fetchInstalledRuleVersions(); + const latestRuleVersions = await ruleAssetsClient.fetchLatestVersions(); + const currentRuleVersionsMap = new Map( + currentRuleVersions.map((rule) => [rule.rule_id, rule]) + ); + const latestRuleVersionsMap = new Map( + latestRuleVersions.map((rule) => [rule.rule_id, rule]) + ); + const installableRules = latestRuleVersions.filter( + (rule) => !currentRuleVersionsMap.has(rule.rule_id) + ); + const upgradeableRules = currentRuleVersions.filter((rule) => { + const latestVersion = latestRuleVersionsMap.get(rule.rule_id); + return latestVersion != null && rule.version < latestVersion.version; }); - const { currentRules, installableRules, upgradeableRules, totalAvailableRules } = - getRuleGroups(ruleVersionsMap); const body: GetPrebuiltRulesStatusResponseBody = { stats: { - num_prebuilt_rules_installed: currentRules.length, + num_prebuilt_rules_installed: currentRuleVersions.length, num_prebuilt_rules_to_install: installableRules.length, num_prebuilt_rules_to_upgrade: upgradeableRules.length, - num_prebuilt_rules_total_in_package: totalAvailableRules.length, + num_prebuilt_rules_total_in_package: latestRuleVersions.length, + }, + aggregated_fields: { + upgradeable_rules: { + tags: [...new Set(upgradeableRules.flatMap((rule) => rule.tags))], + }, }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.test.ts deleted file mode 100644 index 5b1c74825102c..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.test.ts +++ /dev/null @@ -1,191 +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 { getUpgradeableRules } from './get_upgradeable_rules'; -import { ModeEnum, SkipRuleUpgradeReasonEnum } from '../../../../../../common/api/detection_engine'; -import type { - RuleResponse, - RuleUpgradeSpecifier, -} from '../../../../../../common/api/detection_engine'; -import { getPrebuiltRuleMockOfType } from '../../model/rule_assets/prebuilt_rule_asset.mock'; -import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; -import type { RuleTriad } from '../../model/rule_groups/get_rule_groups'; - -describe('getUpgradeableRules', () => { - const baseRule = getPrebuiltRuleMockOfType('query'); - const createUpgradeableRule = ( - ruleId: string, - currentVersion: number, - targetVersion: number - ): RuleTriad => { - return { - current: { - ...baseRule, - rule_id: ruleId, - version: currentVersion, - revision: 0, - }, - target: { ...baseRule, rule_id: ruleId, version: targetVersion }, - } as RuleTriad; - }; - - const mockUpgradeableRule = createUpgradeableRule('rule-1', 1, 2); - - const mockCurrentRule: RuleResponse = { - ...convertPrebuiltRuleAssetToRuleResponse(baseRule), - rule_id: 'rule-1', - revision: 0, - version: 1, - }; - - describe('ALL_RULES mode', () => { - it('should return all upgradeable rules when in ALL_RULES mode', () => { - const result = getUpgradeableRules({ - rawUpgradeableRules: [mockUpgradeableRule], - currentRules: [mockCurrentRule], - mode: ModeEnum.ALL_RULES, - }); - - expect(result.upgradeableRules).toEqual([mockUpgradeableRule]); - expect(result.fetchErrors).toEqual([]); - expect(result.skippedRules).toEqual([]); - }); - - it('should handle empty upgradeable rules list', () => { - const result = getUpgradeableRules({ - rawUpgradeableRules: [], - currentRules: [], - mode: ModeEnum.ALL_RULES, - }); - - expect(result.upgradeableRules).toEqual([]); - expect(result.fetchErrors).toEqual([]); - expect(result.skippedRules).toEqual([]); - }); - }); - - describe('SPECIFIC_RULES mode', () => { - const mockVersionSpecifier: RuleUpgradeSpecifier = { - rule_id: 'rule-1', - revision: 0, - version: 1, - }; - - it('should return specified upgradeable rules when in SPECIFIC_RULES mode', () => { - const result = getUpgradeableRules({ - rawUpgradeableRules: [mockUpgradeableRule], - currentRules: [mockCurrentRule], - versionSpecifiers: [mockVersionSpecifier], - mode: ModeEnum.SPECIFIC_RULES, - }); - - expect(result.upgradeableRules).toEqual([mockUpgradeableRule]); - expect(result.fetchErrors).toEqual([]); - expect(result.skippedRules).toEqual([]); - }); - - it('should handle rule not found', () => { - const result = getUpgradeableRules({ - rawUpgradeableRules: [mockUpgradeableRule], - currentRules: [mockCurrentRule], - versionSpecifiers: [{ ...mockVersionSpecifier, rule_id: 'nonexistent' }], - mode: ModeEnum.SPECIFIC_RULES, - }); - - expect(result.upgradeableRules).toEqual([mockUpgradeableRule]); - expect(result.fetchErrors).toHaveLength(1); - expect(result.fetchErrors[0].error.message).toContain( - 'Rule with rule_id "nonexistent" and version "1" not found' - ); - expect(result.skippedRules).toEqual([]); - }); - - it('should handle non-upgradeable rule', () => { - const nonUpgradeableRule: RuleResponse = { - ...convertPrebuiltRuleAssetToRuleResponse(baseRule), - rule_id: 'rule-2', - revision: 0, - version: 1, - }; - - const result = getUpgradeableRules({ - rawUpgradeableRules: [mockUpgradeableRule], - currentRules: [mockCurrentRule, nonUpgradeableRule], - versionSpecifiers: [mockVersionSpecifier, { ...mockVersionSpecifier, rule_id: 'rule-2' }], - mode: ModeEnum.SPECIFIC_RULES, - }); - - expect(result.upgradeableRules).toEqual([mockUpgradeableRule]); - expect(result.fetchErrors).toEqual([]); - expect(result.skippedRules).toEqual([ - { rule_id: 'rule-2', reason: SkipRuleUpgradeReasonEnum.RULE_UP_TO_DATE }, - ]); - }); - - it('should handle revision mismatch', () => { - const result = getUpgradeableRules({ - rawUpgradeableRules: [mockUpgradeableRule], - currentRules: [mockCurrentRule], - versionSpecifiers: [{ ...mockVersionSpecifier, revision: 1 }], - mode: ModeEnum.SPECIFIC_RULES, - }); - - expect(result.upgradeableRules).toEqual([]); - expect(result.fetchErrors).toHaveLength(1); - expect(result.fetchErrors[0].error.message).toContain( - 'Revision mismatch for rule_id rule-1: expected 0, got 1' - ); - expect(result.skippedRules).toEqual([]); - }); - - it('should handle multiple rules with mixed scenarios', () => { - const mockUpgradeableRule2 = createUpgradeableRule('rule-2', 1, 2); - const mockCurrentRule2: RuleResponse = { - ...convertPrebuiltRuleAssetToRuleResponse(baseRule), - rule_id: 'rule-2', - revision: 0, - version: 1, - }; - const mockCurrentRule3: RuleResponse = { - ...convertPrebuiltRuleAssetToRuleResponse(baseRule), - rule_id: 'rule-3', - revision: 1, - version: 1, - }; - - const result = getUpgradeableRules({ - rawUpgradeableRules: [ - mockUpgradeableRule, - mockUpgradeableRule2, - createUpgradeableRule('rule-3', 1, 2), - ], - currentRules: [mockCurrentRule, mockCurrentRule2, mockCurrentRule3], - versionSpecifiers: [ - mockVersionSpecifier, - { ...mockVersionSpecifier, rule_id: 'rule-2' }, - { ...mockVersionSpecifier, rule_id: 'rule-3', revision: 0 }, - { ...mockVersionSpecifier, rule_id: 'rule-4' }, - { ...mockVersionSpecifier, rule_id: 'rule-5', revision: 1 }, - ], - mode: ModeEnum.SPECIFIC_RULES, - }); - - expect(result.upgradeableRules).toEqual([mockUpgradeableRule, mockUpgradeableRule2]); - expect(result.fetchErrors).toHaveLength(3); - expect(result.fetchErrors[0].error.message).toContain( - 'Revision mismatch for rule_id rule-3: expected 1, got 0' - ); - expect(result.fetchErrors[1].error.message).toContain( - 'Rule with rule_id "rule-4" and version "1" not found' - ); - expect(result.fetchErrors[2].error.message).toContain( - 'Rule with rule_id "rule-5" and version "1" not found' - ); - expect(result.skippedRules).toEqual([]); - }); - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.ts deleted file mode 100644 index 750561b9858a9..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_upgradeable_rules.ts +++ /dev/null @@ -1,85 +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 { withSecuritySpanSync } from '../../../../../utils/with_security_span'; -import type { - RuleResponse, - RuleUpgradeSpecifier, - SkippedRuleUpgrade, -} from '../../../../../../common/api/detection_engine'; -import { ModeEnum, SkipRuleUpgradeReasonEnum } from '../../../../../../common/api/detection_engine'; -import type { PromisePoolError } from '../../../../../utils/promise_pool'; -import type { Mode } from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { RuleTriad } from '../../model/rule_groups/get_rule_groups'; - -export const getUpgradeableRules = ({ - rawUpgradeableRules, - currentRules, - versionSpecifiers, - mode, -}: { - rawUpgradeableRules: RuleTriad[]; - currentRules: RuleResponse[]; - versionSpecifiers?: RuleUpgradeSpecifier[]; - mode: Mode; -}) => { - return withSecuritySpanSync(getUpgradeableRules.name, () => { - const upgradeableRules = new Map( - rawUpgradeableRules.map((_rule) => [_rule.current.rule_id, _rule]) - ); - const fetchErrors: Array> = []; - const skippedRules: SkippedRuleUpgrade[] = []; - - if (mode === ModeEnum.SPECIFIC_RULES) { - const installedRuleIds = new Set(currentRules.map((rule) => rule.rule_id)); - const upgradeableRuleIds = new Set(rawUpgradeableRules.map(({ current }) => current.rule_id)); - versionSpecifiers?.forEach((rule) => { - // Check that the requested rule was found - if (!installedRuleIds.has(rule.rule_id)) { - fetchErrors.push({ - error: new Error( - `Rule with rule_id "${rule.rule_id}" and version "${rule.version}" not found` - ), - item: rule, - }); - return; - } - - // Check that the requested rule is upgradeable - if (!upgradeableRuleIds.has(rule.rule_id)) { - skippedRules.push({ - rule_id: rule.rule_id, - reason: SkipRuleUpgradeReasonEnum.RULE_UP_TO_DATE, - }); - return; - } - - // Check that rule revisions match (no update slipped in since the user reviewed the list) - const currentRevision = currentRules.find( - (currentRule) => currentRule.rule_id === rule.rule_id - )?.revision; - if (rule.revision !== currentRevision) { - fetchErrors.push({ - error: new Error( - `Revision mismatch for rule_id ${rule.rule_id}: expected ${currentRevision}, got ${rule.revision}` - ), - item: rule, - }); - // Remove the rule from the list of upgradeable rules - if (upgradeableRules.has(rule.rule_id)) { - upgradeableRules.delete(rule.rule_id); - } - } - }); - } - - return { - upgradeableRules: Array.from(upgradeableRules.values()), - fetchErrors, - skippedRules, - }; - }); -}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts new file mode 100644 index 0000000000000..6bea2c71123da --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_handler.ts @@ -0,0 +1,235 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import type { + PerformRuleUpgradeRequestBody, + PerformRuleUpgradeResponseBody, + SkippedRuleUpgrade, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { + ModeEnum, + PickVersionValuesEnum, + SkipRuleUpgradeReasonEnum, + UpgradeConflictResolutionEnum, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import type { SecuritySolutionRequestHandlerContext } from '../../../../../types'; +import { buildSiemResponse } from '../../../routes/utils'; +import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; +import { aggregatePrebuiltRuleErrors } from '../../logic/aggregate_prebuilt_rule_errors'; +import { performTimelinesInstallation } from '../../logic/perform_timelines_installation'; +import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; +import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; +import { upgradePrebuiltRules } from '../../logic/rule_objects/upgrade_prebuilt_rules'; +import { createModifiedPrebuiltRuleAssets } from './create_upgradeable_rules_payload'; +import { validatePerformRuleUpgradeRequest } from './validate_perform_rule_upgrade_request'; +import type { + RuleResponse, + RuleSignatureId, + RuleVersion, +} from '../../../../../../common/api/detection_engine'; +import type { PromisePoolError } from '../../../../../utils/promise_pool'; +import { zipRuleVersions } from '../../logic/rule_versions/zip_rule_versions'; +import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff'; +import type { RuleTriad } from '../../model/rule_groups/get_rule_groups'; + +export const performRuleUpgradeHandler = async ( + context: SecuritySolutionRequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory +) => { + const siemResponse = buildSiemResponse(response); + + try { + const ctx = await context.resolve(['core', 'alerting', 'securitySolution']); + const soClient = ctx.core.savedObjects.client; + const rulesClient = await ctx.alerting.getRulesClient(); + const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient(); + const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); + const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); + + const { isRulesCustomizationEnabled } = detectionRulesClient.getRuleCustomizationStatus(); + const defaultPickVersion = isRulesCustomizationEnabled + ? PickVersionValuesEnum.MERGED + : PickVersionValuesEnum.TARGET; + + validatePerformRuleUpgradeRequest({ + isRulesCustomizationEnabled, + payload: request.body, + defaultPickVersion, + }); + + const { mode, dry_run: isDryRun, on_conflict: onConflict } = request.body; + + const filter = mode === ModeEnum.ALL_RULES ? request.body.filter : undefined; + + const skippedRules: SkippedRuleUpgrade[] = []; + const updatedRules: RuleResponse[] = []; + const ruleErrors: Array> = []; + const allErrors: PerformRuleUpgradeResponseBody['errors'] = []; + + const ruleUpgradeQueue: Array<{ + rule_id: RuleSignatureId; + version: RuleVersion; + revision?: number; + }> = []; + + if (mode === ModeEnum.ALL_RULES) { + const allLatestVersions = await ruleAssetsClient.fetchLatestVersions(); + const latestVersionsMap = new Map( + allLatestVersions.map((version) => [version.rule_id, version]) + ); + const allCurrentVersions = await ruleObjectsClient.fetchInstalledRuleVersions({ + filter, + }); + + allCurrentVersions.forEach((current) => { + const latest = latestVersionsMap.get(current.rule_id); + if (latest && latest.version > current.version) { + ruleUpgradeQueue.push({ + rule_id: current.rule_id, + version: latest.version, + }); + } + }); + } else if (mode === ModeEnum.SPECIFIC_RULES) { + ruleUpgradeQueue.push(...request.body.rules); + } + + const BATCH_SIZE = 100; + while (ruleUpgradeQueue.length > 0) { + const targetRulesForUpgrade = ruleUpgradeQueue.splice(0, BATCH_SIZE); + + const [currentRules, latestRules] = await Promise.all([ + ruleObjectsClient.fetchInstalledRulesByIds({ + ruleIds: targetRulesForUpgrade.map(({ rule_id: ruleId }) => ruleId), + }), + ruleAssetsClient.fetchAssetsByVersion(targetRulesForUpgrade), + ]); + const baseRules = await ruleAssetsClient.fetchAssetsByVersion(currentRules); + const ruleVersionsMap = zipRuleVersions(currentRules, baseRules, latestRules); + + const upgradeableRules: RuleTriad[] = []; + targetRulesForUpgrade.forEach((targetRule) => { + const ruleVersions = ruleVersionsMap.get(targetRule.rule_id); + + const currentVersion = ruleVersions?.current; + const baseVersion = ruleVersions?.base; + const targetVersion = ruleVersions?.target; + + // Check that the requested rule was found + if (!currentVersion) { + ruleErrors.push({ + error: new Error( + `Rule with rule_id "${targetRule.rule_id}" and version "${targetRule.version}" not found` + ), + item: targetRule, + }); + return; + } + + // Check that the requested rule is upgradeable + if (!targetVersion || targetVersion.version <= currentVersion.version) { + skippedRules.push({ + rule_id: targetRule.rule_id, + reason: SkipRuleUpgradeReasonEnum.RULE_UP_TO_DATE, + }); + return; + } + + // Check that rule revisions match (no update slipped in since the user reviewed the list) + if (targetRule.revision != null && targetRule.revision !== currentVersion.revision) { + ruleErrors.push({ + error: new Error( + `Revision mismatch for rule_id ${targetRule.rule_id}: expected ${currentVersion.revision}, got ${targetRule.revision}` + ), + item: targetRule, + }); + return; + } + + // Check there's no conflicts + if (onConflict === UpgradeConflictResolutionEnum.SKIP) { + const ruleDiff = calculateRuleDiff(ruleVersions); + const hasConflict = ruleDiff.ruleDiff.num_fields_with_conflicts > 0; + if (hasConflict) { + skippedRules.push({ + rule_id: targetRule.rule_id, + reason: SkipRuleUpgradeReasonEnum.CONFLICT, + }); + return; + } + } + + // All checks passed, add to the list of rules to upgrade + upgradeableRules.push({ + current: currentVersion, + base: baseVersion, + target: targetVersion, + }); + }); + + const { modifiedPrebuiltRuleAssets, processingErrors } = createModifiedPrebuiltRuleAssets({ + upgradeableRules, + requestBody: request.body, + defaultPickVersion, + }); + ruleErrors.push(...processingErrors); + + if (isDryRun) { + updatedRules.push( + ...modifiedPrebuiltRuleAssets.map((rule) => convertPrebuiltRuleAssetToRuleResponse(rule)) + ); + } else { + const { results: upgradeResults, errors: installationErrors } = await upgradePrebuiltRules( + detectionRulesClient, + modifiedPrebuiltRuleAssets + ); + ruleErrors.push(...installationErrors); + updatedRules.push(...upgradeResults.map(({ result }) => result)); + } + } + + allErrors.push(...aggregatePrebuiltRuleErrors(ruleErrors)); + + if (!isDryRun) { + const { error: timelineInstallationError } = await performTimelinesInstallation( + ctx.securitySolution + ); + + if (timelineInstallationError) { + allErrors.push({ + message: timelineInstallationError, + rules: [], + }); + } + } + + const body: PerformRuleUpgradeResponseBody = { + summary: { + total: updatedRules.length + skippedRules.length + ruleErrors.length, + skipped: skippedRules.length, + succeeded: updatedRules.length, + failed: ruleErrors.length, + }, + results: { + updated: updatedRules, + skipped: skippedRules, + }, + errors: allErrors, + }; + + return response.ok({ body }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts index 2c10d62d570db..4ab73ab2e8e64 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts @@ -5,32 +5,18 @@ * 2.0. */ -import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { PERFORM_RULE_UPGRADE_URL, PerformRuleUpgradeRequestBody, - ModeEnum, - PickVersionValuesEnum, } from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { PerformRuleUpgradeResponseBody } from '../../../../../../common/api/detection_engine/prebuilt_rules'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import { buildSiemResponse } from '../../../routes/utils'; -import { aggregatePrebuiltRuleErrors } from '../../logic/aggregate_prebuilt_rule_errors'; -import { performTimelinesInstallation } from '../../logic/perform_timelines_installation'; -import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; -import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; -import { upgradePrebuiltRules } from '../../logic/rule_objects/upgrade_prebuilt_rules'; -import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; +import { routeLimitedConcurrencyTag } from '../../../../../utils/route_limited_concurrency_tag'; import { - PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, PREBUILT_RULES_OPERATION_CONCURRENCY, + PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, } from '../../constants'; -import { getUpgradeableRules } from './get_upgradeable_rules'; -import { createModifiedPrebuiltRuleAssets } from './create_upgradeable_rules_payload'; -import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; -import { validatePerformRuleUpgradeRequest } from './validate_perform_rule_upgrade_request'; -import { routeLimitedConcurrencyTag } from '../../../../../utils/route_limited_concurrency_tag'; +import { performRuleUpgradeHandler } from './perform_rule_upgrade_handler'; export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => { router.versioned @@ -58,93 +44,6 @@ export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => }, }, }, - async (context, request, response) => { - const siemResponse = buildSiemResponse(response); - - try { - const ctx = await context.resolve(['core', 'alerting', 'securitySolution', 'licensing']); - const soClient = ctx.core.savedObjects.client; - const rulesClient = await ctx.alerting.getRulesClient(); - const detectionRulesClient = ctx.securitySolution.getDetectionRulesClient(); - const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); - const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); - - const { isRulesCustomizationEnabled } = detectionRulesClient.getRuleCustomizationStatus(); - const defaultPickVersion = isRulesCustomizationEnabled - ? PickVersionValuesEnum.MERGED - : PickVersionValuesEnum.TARGET; - - validatePerformRuleUpgradeRequest({ - isRulesCustomizationEnabled, - payload: request.body, - defaultPickVersion, - }); - - const { mode } = request.body; - - const versionSpecifiers = mode === ModeEnum.ALL_RULES ? undefined : request.body.rules; - const ruleTriadsMap = await fetchRuleVersionsTriad({ - ruleAssetsClient, - ruleObjectsClient, - versionSpecifiers, - }); - const ruleGroups = getRuleGroups(ruleTriadsMap); - - const { upgradeableRules, skippedRules, fetchErrors } = getUpgradeableRules({ - rawUpgradeableRules: ruleGroups.upgradeableRules, - currentRules: ruleGroups.currentRules, - versionSpecifiers, - mode, - }); - - const { modifiedPrebuiltRuleAssets, processingErrors } = createModifiedPrebuiltRuleAssets( - { - upgradeableRules, - requestBody: request.body, - defaultPickVersion, - } - ); - - const { results: updatedRules, errors: installationErrors } = await upgradePrebuiltRules( - detectionRulesClient, - modifiedPrebuiltRuleAssets - ); - const ruleErrors = [...fetchErrors, ...processingErrors, ...installationErrors]; - - const { error: timelineInstallationError } = await performTimelinesInstallation( - ctx.securitySolution - ); - - const allErrors = aggregatePrebuiltRuleErrors(ruleErrors); - if (timelineInstallationError) { - allErrors.push({ - message: timelineInstallationError, - rules: [], - }); - } - - const body: PerformRuleUpgradeResponseBody = { - summary: { - total: updatedRules.length + skippedRules.length + ruleErrors.length, - skipped: skippedRules.length, - succeeded: updatedRules.length, - failed: ruleErrors.length, - }, - results: { - updated: updatedRules.map(({ result }) => result), - skipped: skippedRules, - }, - errors: allErrors, - }; - - return response.ok({ body }); - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); - } - } + performRuleUpgradeHandler ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts index 4d35b98718a98..0bf1ee5cfc80a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_handler.ts @@ -16,9 +16,7 @@ import { buildSiemResponse } from '../../../routes/utils'; import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; -import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; -import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; export const reviewRuleInstallationHandler = async ( context: SecuritySolutionRequestHandlerContext, @@ -34,15 +32,21 @@ export const reviewRuleInstallationHandler = async ( const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); - const ruleVersionsMap = await fetchRuleVersionsTriad({ - ruleAssetsClient, - ruleObjectsClient, + const allLatestVersions = await ruleAssetsClient.fetchLatestVersions(); + const currentRuleVersions = await ruleObjectsClient.fetchInstalledRuleVersions(); + const currentRuleVersionsMap = new Map( + currentRuleVersions.map((version) => [version.rule_id, version]) + ); + + const installableRules = allLatestVersions.filter((latestVersion) => { + const currentVersion = currentRuleVersionsMap.get(latestVersion.rule_id); + return !currentVersion; }); - const { installableRules } = getRuleGroups(ruleVersionsMap); + const installableRuleAssets = await ruleAssetsClient.fetchAssetsByVersion(installableRules); const body: ReviewRuleInstallationResponseBody = { - stats: calculateRuleStats(installableRules), - rules: installableRules.map((prebuiltRuleAsset) => + stats: calculateRuleStats(installableRuleAssets), + rules: installableRuleAssets.map((prebuiltRuleAsset) => convertPrebuiltRuleAssetToRuleResponse(prebuiltRuleAsset) ), }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts new file mode 100644 index 0000000000000..1bfd9f09c43f7 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/calculate_rule_upgrade_info.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pickBy } from 'lodash'; +import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; +import type { + RuleUpgradeInfoForReview, + ThreeWayDiff, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { ThreeWayDiffOutcome } from '../../../../../../common/api/detection_engine/prebuilt_rules'; +import { invariant } from '../../../../../../common/utils/invariant'; +import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; +import type { CalculateRuleDiffResult } from '../../logic/diff/calculate_rule_diff'; + +export const calculateRuleUpgradeInfo = ( + results: CalculateRuleDiffResult[] +): RuleUpgradeInfoForReview[] => { + return results.map((result) => { + const { ruleDiff, ruleVersions } = result; + const installedCurrentVersion = ruleVersions.input.current; + const targetVersion = ruleVersions.input.target; + invariant(installedCurrentVersion != null, 'installedCurrentVersion not found'); + invariant(targetVersion != null, 'targetVersion not found'); + + const targetRule: RuleResponse = { + ...convertPrebuiltRuleAssetToRuleResponse(targetVersion), + id: installedCurrentVersion.id, + revision: installedCurrentVersion.revision + 1, + created_at: installedCurrentVersion.created_at, + created_by: installedCurrentVersion.created_by, + updated_at: new Date().toISOString(), + updated_by: installedCurrentVersion.updated_by, + }; + + return { + id: installedCurrentVersion.id, + rule_id: installedCurrentVersion.rule_id, + revision: installedCurrentVersion.revision, + version: installedCurrentVersion.version, + current_rule: installedCurrentVersion, + target_rule: targetRule, + diff: { + fields: pickBy>( + ruleDiff.fields, + (fieldDiff) => + fieldDiff.diff_outcome !== ThreeWayDiffOutcome.StockValueNoUpdate && + fieldDiff.diff_outcome !== ThreeWayDiffOutcome.MissingBaseNoUpdate + ), + num_fields_with_updates: ruleDiff.num_fields_with_updates, + num_fields_with_conflicts: ruleDiff.num_fields_with_conflicts, + num_fields_with_non_solvable_conflicts: ruleDiff.num_fields_with_non_solvable_conflicts, + }, + }; + }); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts index 9f4fa7ddc766e..05a52dbf0ed41 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_handler.ts @@ -7,32 +7,35 @@ import type { KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; import { transformError } from '@kbn/securitysolution-es-utils'; -import { pickBy } from 'lodash'; -import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema'; +import type { ReviewPrebuiltRuleUpgradeFilter } from '../../../../../../common/api/detection_engine/prebuilt_rules/common/review_prebuilt_rules_upgrade_filter'; import type { + ReviewRuleUpgradeRequestBody, ReviewRuleUpgradeResponseBody, - RuleUpgradeInfoForReview, - RuleUpgradeStatsForReview, - ThreeWayDiff, + ReviewRuleUpgradeSort, } from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import { ThreeWayDiffOutcome } from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import { invariant } from '../../../../../../common/utils/invariant'; import type { SecuritySolutionRequestHandlerContext } from '../../../../../types'; import { buildSiemResponse } from '../../../routes/utils'; -import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/logic/detection_rules_client/converters/convert_prebuilt_rule_asset_to_rule_response'; -import type { CalculateRuleDiffResult } from '../../logic/diff/calculate_rule_diff'; import { calculateRuleDiff } from '../../logic/diff/calculate_rule_diff'; +import type { IPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client'; +import type { IPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client'; -import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad'; -import { getRuleGroups } from '../../model/rule_groups/get_rule_groups'; +import type { RuleVersionSpecifier } from '../../logic/rule_versions/rule_version_specifier'; +import { zipRuleVersions } from '../../logic/rule_versions/zip_rule_versions'; +import { calculateRuleUpgradeInfo } from './calculate_rule_upgrade_info'; + +const DEFAULT_SORT: ReviewRuleUpgradeSort = { + field: 'name', + order: 'asc', +}; export const reviewRuleUpgradeHandler = async ( context: SecuritySolutionRequestHandlerContext, - request: KibanaRequest, + request: KibanaRequest, response: KibanaResponseFactory ) => { const siemResponse = buildSiemResponse(response); + const { page = 1, per_page: perPage = 20, sort = DEFAULT_SORT, filter } = request.body ?? {}; try { const ctx = await context.resolve(['core', 'alerting']); @@ -41,21 +44,26 @@ export const reviewRuleUpgradeHandler = async ( const ruleAssetsClient = createPrebuiltRuleAssetsClient(soClient); const ruleObjectsClient = createPrebuiltRuleObjectsClient(rulesClient); - const ruleVersionsMap = await fetchRuleVersionsTriad({ + const { diffResults, totalUpgradeableRules } = await calculateUpgradeableRulesDiff({ ruleAssetsClient, ruleObjectsClient, - }); - const { upgradeableRules } = getRuleGroups(ruleVersionsMap); - - const ruleDiffCalculationResults = upgradeableRules.map(({ current }) => { - const ruleVersions = ruleVersionsMap.get(current.rule_id); - invariant(ruleVersions != null, 'ruleVersions not found'); - return calculateRuleDiff(ruleVersions); + page, + perPage, + sort, + filter, }); const body: ReviewRuleUpgradeResponseBody = { - stats: calculateRuleStats(ruleDiffCalculationResults), - rules: calculateRuleInfos(ruleDiffCalculationResults), + stats: { + num_rules_to_upgrade_total: 0, + num_rules_with_conflicts: 0, + num_rules_with_non_solvable_conflicts: 0, + tags: [], + }, + rules: calculateRuleUpgradeInfo(diffResults), + page, + per_page: perPage, + total: totalUpgradeableRules, }; return response.ok({ body }); @@ -67,72 +75,68 @@ export const reviewRuleUpgradeHandler = async ( }); } }; -const calculateRuleStats = (results: CalculateRuleDiffResult[]): RuleUpgradeStatsForReview => { - const allTags = new Set(); - const stats = results.reduce( - (acc, result) => { - acc.num_rules_to_upgrade_total += 1; +interface CalculateUpgradeableRulesDiffArgs { + ruleAssetsClient: IPrebuiltRuleAssetsClient; + ruleObjectsClient: IPrebuiltRuleObjectsClient; + page: number; + perPage: number; + sort: ReviewRuleUpgradeSort; + filter: ReviewPrebuiltRuleUpgradeFilter | undefined; +} - if (result.ruleDiff.num_fields_with_conflicts > 0) { - acc.num_rules_with_conflicts += 1; - } +async function calculateUpgradeableRulesDiff({ + ruleAssetsClient, + ruleObjectsClient, + page, + perPage, + sort, + filter, +}: CalculateUpgradeableRulesDiffArgs) { + const allLatestVersions = await ruleAssetsClient.fetchLatestVersions(); + const latestVersionsMap = new Map(allLatestVersions.map((version) => [version.rule_id, version])); - if (result.ruleDiff.num_fields_with_non_solvable_conflicts > 0) { - acc.num_rules_with_non_solvable_conflicts += 1; - } + const currentRuleVersions = filter?.rule_ids + ? await ruleObjectsClient.fetchInstalledRuleVersionsByIds({ + ruleIds: filter.rule_ids, + sortField: sort.field, + sortOrder: sort.order, + }) + : await ruleObjectsClient.fetchInstalledRuleVersions({ + filter, + sortField: sort.field, + sortOrder: sort.order, + }); + const upgradeableRuleIds = currentRuleVersions + .filter((rule) => { + const targetVersion = latestVersionsMap.get(rule.rule_id); + return targetVersion != null && rule.version < targetVersion.version; + }) + .map((rule) => rule.rule_id); + const totalUpgradeableRules = upgradeableRuleIds.length; - result.ruleVersions.input.current?.tags.forEach((tag) => allTags.add(tag)); - - return acc; - }, - { - num_rules_to_upgrade_total: 0, - num_rules_with_conflicts: 0, - num_rules_with_non_solvable_conflicts: 0, - } + const pagedRuleIds = upgradeableRuleIds.slice((page - 1) * perPage, page * perPage); + const currentRules = await ruleObjectsClient.fetchInstalledRulesByIds({ + ruleIds: pagedRuleIds, + sortField: sort.field, + sortOrder: sort.order, + }); + const latestRules = await ruleAssetsClient.fetchAssetsByVersion( + currentRules.map(({ rule_id: ruleId }) => latestVersionsMap.get(ruleId) as RuleVersionSpecifier) ); + const baseRules = await ruleAssetsClient.fetchAssetsByVersion(currentRules); + const ruleVersionsMap = zipRuleVersions(currentRules, baseRules, latestRules); + + // Calculate the diff between current, base, and target versions + // Iterate through the current rules array to keep the order of the results + const diffResults = currentRules.map((current) => { + const base = ruleVersionsMap.get(current.rule_id)?.base; + const target = ruleVersionsMap.get(current.rule_id)?.target; + return calculateRuleDiff({ current, base, target }); + }); return { - ...stats, - tags: Array.from(allTags), + diffResults, + totalUpgradeableRules, }; -}; -const calculateRuleInfos = (results: CalculateRuleDiffResult[]): RuleUpgradeInfoForReview[] => { - return results.map((result) => { - const { ruleDiff, ruleVersions } = result; - const installedCurrentVersion = ruleVersions.input.current; - const targetVersion = ruleVersions.input.target; - invariant(installedCurrentVersion != null, 'installedCurrentVersion not found'); - invariant(targetVersion != null, 'targetVersion not found'); - - const targetRule: RuleResponse = { - ...convertPrebuiltRuleAssetToRuleResponse(targetVersion), - id: installedCurrentVersion.id, - revision: installedCurrentVersion.revision + 1, - created_at: installedCurrentVersion.created_at, - created_by: installedCurrentVersion.created_by, - updated_at: new Date().toISOString(), - updated_by: installedCurrentVersion.updated_by, - }; - - return { - id: installedCurrentVersion.id, - rule_id: installedCurrentVersion.rule_id, - revision: installedCurrentVersion.revision, - current_rule: installedCurrentVersion, - target_rule: targetRule, - diff: { - fields: pickBy>( - ruleDiff.fields, - (fieldDiff) => - fieldDiff.diff_outcome !== ThreeWayDiffOutcome.StockValueNoUpdate && - fieldDiff.diff_outcome !== ThreeWayDiffOutcome.MissingBaseNoUpdate - ), - num_fields_with_updates: ruleDiff.num_fields_with_updates, - num_fields_with_conflicts: ruleDiff.num_fields_with_conflicts, - num_fields_with_non_solvable_conflicts: ruleDiff.num_fields_with_non_solvable_conflicts, - }, - }; - }); -}; +} diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts index 54f51752b7ab8..24b3865fc5ea0 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts @@ -5,12 +5,16 @@ * 2.0. */ -import { REVIEW_RULE_UPGRADE_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules'; -import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { + REVIEW_RULE_UPGRADE_URL, + ReviewRuleUpgradeRequestBody, +} from '../../../../../../common/api/detection_engine/prebuilt_rules'; import { routeLimitedConcurrencyTag } from '../../../../../utils/route_limited_concurrency_tag'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { - PREBUILT_RULES_OPERATION_CONCURRENCY, PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, + PREBUILT_RULES_UPGRADE_REVIEW_CONCURRENCY, } from '../../constants'; import { reviewRuleUpgradeHandler } from './review_rule_upgrade_handler'; @@ -25,7 +29,7 @@ export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => }, }, options: { - tags: [routeLimitedConcurrencyTag(PREBUILT_RULES_OPERATION_CONCURRENCY)], + tags: [routeLimitedConcurrencyTag(PREBUILT_RULES_UPGRADE_REVIEW_CONCURRENCY)], timeout: { idleSocket: PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, }, @@ -34,7 +38,11 @@ export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => .addVersion( { version: '1', - validate: {}, + validate: { + request: { + body: buildRouteValidationWithZod(ReviewRuleUpgradeRequestBody), + }, + }, }, reviewRuleUpgradeHandler ); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/constants.ts index 7d281ae96e321..86fd9b45025dc 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/constants.ts @@ -10,3 +10,11 @@ export const PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS = 1_800_000 as const; // // Only one rule installation or upgrade request can be processed at a time. // Multiple requests can lead to high memory usage and unexpected behavior. export const PREBUILT_RULES_OPERATION_CONCURRENCY = 1; + +/** + * Prebuilt rules upgrade review API endpoint max concurrency. + * + * It differs from PREBUILT_RULES_OPERATION_CONCURRENCY since upgrade review API endpoint + * is expected to be requested much more often than the other prebuilt rules API endpoints. + */ +export const PREBUILT_RULES_UPGRADE_REVIEW_CONCURRENCY = 3; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts index 1138a48cc39d4..2bf7cf5c44364 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/prebuilt_rule_objects_client.ts @@ -9,36 +9,73 @@ import type { RulesClient } from '@kbn/alerting-plugin/server'; import type { RuleResponse, RuleSignatureId, + RuleTagArray, } from '../../../../../../common/api/detection_engine/model/rule_schema'; import { withSecuritySpan } from '../../../../../utils/with_security_span'; import { findRules } from '../../../rule_management/logic/search/find_rules'; -import { getExistingPrepackagedRules } from '../../../rule_management/logic/search/get_existing_prepackaged_rules'; import { internalRuleToAPIResponse } from '../../../rule_management/logic/detection_rules_client/converters/internal_rule_to_api_response'; +import { convertRulesFilterToKQL } from '../../../../../../common/detection_engine/rule_management/rule_filtering'; +import type { + FindRulesSortField, + PrebuiltRulesFilter, + SortOrder, +} from '../../../../../../common/api/detection_engine'; +import { MAX_PREBUILT_RULES_COUNT } from '../../../rule_management/logic/search/get_existing_prepackaged_rules'; +import type { RuleVersionSpecifier } from '../rule_versions/rule_version_specifier'; + +interface FetchAllInstalledRulesArgs { + page?: number; + perPage?: number; + filter?: PrebuiltRulesFilter; + sortField?: FindRulesSortField; + sortOrder?: SortOrder; +} + +interface FetchAllInstalledRuleVersionsArgs { + filter?: PrebuiltRulesFilter; + sortField?: FindRulesSortField; + sortOrder?: SortOrder; +} + +interface FetchInstalledRuleVersionsByIdsArgs { + ruleIds: RuleSignatureId[]; + sortField?: FindRulesSortField; + sortOrder?: SortOrder; +} + +interface FetchInstalledRulesByIdsArgs { + ruleIds: RuleSignatureId[]; + sortField?: FindRulesSortField; + sortOrder?: SortOrder; +} export interface IPrebuiltRuleObjectsClient { - fetchAllInstalledRules(): Promise; - fetchInstalledRulesByIds(ruleIds: string[]): Promise; + fetchInstalledRulesByIds(args: FetchInstalledRulesByIdsArgs): Promise; + fetchInstalledRules(args?: FetchAllInstalledRulesArgs): Promise; + fetchInstalledRuleVersionsByIds( + args: FetchInstalledRuleVersionsByIdsArgs + ): Promise>; + fetchInstalledRuleVersions( + args?: FetchAllInstalledRuleVersionsArgs + ): Promise>; } export const createPrebuiltRuleObjectsClient = ( rulesClient: RulesClient ): IPrebuiltRuleObjectsClient => { return { - fetchAllInstalledRules: (): Promise => { - return withSecuritySpan('IPrebuiltRuleObjectsClient.fetchInstalledRules', async () => { - const rulesData = await getExistingPrepackagedRules({ rulesClient }); - const rules = rulesData.map((rule) => internalRuleToAPIResponse(rule)); - return rules; - }); - }, - fetchInstalledRulesByIds: (ruleIds: RuleSignatureId[]): Promise => { + fetchInstalledRulesByIds: ({ ruleIds, sortField = 'createdAt', sortOrder = 'desc' }) => { return withSecuritySpan('IPrebuiltRuleObjectsClient.fetchInstalledRulesByIds', async () => { + if (ruleIds.length === 0) { + return []; + } + const { data } = await findRules({ rulesClient, perPage: ruleIds.length, page: 1, - sortField: 'createdAt', - sortOrder: 'desc', + sortField, + sortOrder, fields: undefined, filter: `alert.attributes.params.ruleId:(${ruleIds.join(' or ')})`, }); @@ -47,5 +84,78 @@ export const createPrebuiltRuleObjectsClient = ( return rules; }); }, + fetchInstalledRules: ({ page, perPage, sortField, sortOrder, filter } = {}) => { + return withSecuritySpan('IPrebuiltRuleObjectsClient.fetchInstalledRules', async () => { + const filterKQL = convertRulesFilterToKQL({ + showElasticRules: true, + filter: filter?.name, + tags: filter?.tags, + customizationStatus: filter?.customization_status, + }); + + const rulesData = await findRules({ + rulesClient, + filter: filterKQL, + perPage, + page, + sortField, + sortOrder, + fields: undefined, + }); + const rules = rulesData.data.map((rule) => internalRuleToAPIResponse(rule)); + return rules; + }); + }, + fetchInstalledRuleVersionsByIds: ({ ruleIds, sortField, sortOrder }) => { + return withSecuritySpan( + 'IPrebuiltRuleObjectsClient.fetchInstalledRuleVersionsByIds', + async () => { + const filterKQL = convertRulesFilterToKQL({ + showElasticRules: true, + }); + + const rulesData = await findRules({ + rulesClient, + ruleIds, + filter: filterKQL, + perPage: MAX_PREBUILT_RULES_COUNT, + page: 1, + sortField, + sortOrder, + fields: ['params.ruleId', 'params.version', 'tags'], + }); + return rulesData.data.map((rule) => ({ + rule_id: rule.params.ruleId, + version: rule.params.version, + tags: rule.tags, + })); + } + ); + }, + fetchInstalledRuleVersions: ({ filter, sortField, sortOrder } = {}) => { + return withSecuritySpan('IPrebuiltRuleObjectsClient.fetchInstalledRuleVersions', async () => { + const filterKQL = convertRulesFilterToKQL({ + showElasticRules: true, + filter: filter?.name, + tags: filter?.tags, + customizationStatus: filter?.customization_status, + }); + + const rulesData = await findRules({ + rulesClient, + filter: filterKQL, + perPage: MAX_PREBUILT_RULES_COUNT, + page: 1, + sortField, + sortOrder, + fields: ['params.ruleId', 'params.version', 'tags'], + }); + return rulesData.data.map((rule) => ({ + rule_id: rule.params.ruleId, + version: rule.params.version, + tags: rule.tags, + })); + }); + }, }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts index 11a5660e77a31..d4cd56ae19f32 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_versions/fetch_rule_versions_triad.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { PrebuiltRulesFilter } from '../../../../../../common/api/detection_engine'; +import { MAX_PREBUILT_RULES_COUNT } from '../../../rule_management/logic/search/get_existing_prepackaged_rules'; import type { RuleVersions } from '../diff/calculate_rule_diff'; import type { IPrebuiltRuleAssetsClient } from '../rule_assets/prebuilt_rule_assets_client'; import type { IPrebuiltRuleObjectsClient } from '../rule_objects/prebuilt_rule_objects_client'; @@ -15,19 +17,25 @@ interface GetRuleVersionsMapArgs { ruleObjectsClient: IPrebuiltRuleObjectsClient; ruleAssetsClient: IPrebuiltRuleAssetsClient; versionSpecifiers?: RuleVersionSpecifier[]; + filter?: PrebuiltRulesFilter; } export async function fetchRuleVersionsTriad({ ruleObjectsClient, ruleAssetsClient, versionSpecifiers, + filter, }: GetRuleVersionsMapArgs): Promise> { const [currentRules, latestRules] = await Promise.all([ versionSpecifiers - ? ruleObjectsClient.fetchInstalledRulesByIds( - versionSpecifiers.map(({ rule_id: ruleId }) => ruleId) - ) - : ruleObjectsClient.fetchAllInstalledRules(), + ? ruleObjectsClient.fetchInstalledRulesByIds({ + ruleIds: versionSpecifiers.map(({ rule_id: ruleId }) => ruleId), + }) + : ruleObjectsClient.fetchInstalledRules({ + filter, + page: 1, + perPage: MAX_PREBUILT_RULES_COUNT, + }), versionSpecifiers ? ruleAssetsClient.fetchAssetsByVersion(versionSpecifiers) : ruleAssetsClient.fetchLatestAssets(), diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.ts index 24b2954547e40..9a7eb8fdf6f56 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_management/logic/search/get_existing_prepackaged_rules.ts @@ -49,16 +49,20 @@ export const getRulesCount = async ({ export const getRules = async ({ rulesClient, filter, + page = 1, + perPage = MAX_PREBUILT_RULES_COUNT, }: { rulesClient: RulesClient; filter: string; + page?: number; + perPage?: number; }): Promise => withSecuritySpan('getRules', async () => { const rules = await findRules({ rulesClient, filter, - perPage: MAX_PREBUILT_RULES_COUNT, - page: 1, + perPage, + page, sortField: 'createdAt', sortOrder: 'desc', fields: undefined, @@ -80,11 +84,17 @@ export const getNonPackagedRules = async ({ export const getExistingPrepackagedRules = async ({ rulesClient, + page, + perPage, }: { rulesClient: RulesClient; + page?: number; + perPage?: number; }): Promise => { return getRules({ rulesClient, + page, + perPage, filter: KQL_FILTER_IMMUTABLE_RULES, }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/preview_prebuilt_rules_upgrade.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/preview_prebuilt_rules_upgrade.ts index 9d80d14257c37..d5fbc330ee3e2 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/preview_prebuilt_rules_upgrade.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/prebuilt_rule_customization/customization_enabled/preview_prebuilt_rules_upgrade.ts @@ -11,7 +11,6 @@ import { FtrProviderContext } from '../../../../../../ftr_provider_context'; import { deleteAllPrebuiltRuleAssets, fetchFirstPrebuiltRuleUpgradeReviewDiff, - reviewPrebuiltRulesToUpgrade, } from '../../../../utils'; import { setUpRuleUpgrade } from '../../../../utils/rules/prebuilt_rules/set_up_rule_upgrade'; @@ -36,241 +35,6 @@ export default ({ getService }: FtrProviderContext): void => { describe( withHistoricalVersions ? 'with historical versions' : 'without historical versions', () => { - describe('stats', () => { - it('returns num of rules with upgrades', async () => { - await setUpRuleUpgrade({ - assets: [ - { - installed: { - rule_id: 'query-rule', - type: 'query', - version: 1, - }, - patch: {}, - upgrade: { - rule_id: 'query-rule', - type: 'query', - version: 2, - }, - }, - { - installed: { - rule_id: 'saved-query-rule', - type: 'query', - version: 1, - }, - patch: {}, - upgrade: { - rule_id: 'saved-query-rule', - type: 'query', - version: 2, - }, - }, - ], - removeInstalledAssets: !withHistoricalVersions, - deps, - }); - - const response = await reviewPrebuiltRulesToUpgrade(supertest); - - expect(response.stats).toMatchObject({ - num_rules_to_upgrade_total: 2, - }); - }); - - it('returns zero conflicts when there are no conflicts', async () => { - await setUpRuleUpgrade({ - assets: [ - { - installed: { - rule_id: 'query-rule', - type: 'query', - version: 1, - }, - patch: {}, - upgrade: { - rule_id: 'query-rule', - type: 'query', - version: 2, - }, - }, - { - installed: { - rule_id: 'saved-query-rule', - type: 'query', - version: 1, - }, - patch: {}, - upgrade: { - rule_id: 'saved-query-rule', - type: 'query', - version: 2, - }, - }, - ], - removeInstalledAssets: !withHistoricalVersions, - deps, - }); - - const response = await reviewPrebuiltRulesToUpgrade(supertest); - - expect(response.stats).toMatchObject({ - num_rules_with_conflicts: 0, - num_rules_with_non_solvable_conflicts: 0, - }); - }); - - it('returns num of rules with conflicts', async () => { - await setUpRuleUpgrade({ - assets: [ - { - installed: { - rule_id: 'query-rule', - type: 'query', - name: 'Initial name', - version: 1, - }, - patch: { - rule_id: 'query-rule', - name: 'Customized name', - }, - upgrade: { - rule_id: 'query-rule', - type: 'query', - name: 'Updated name', - version: 2, - }, - }, - { - installed: { - rule_id: 'saved-query-rule', - type: 'query', - tags: ['tagA'], - version: 1, - }, - patch: { - rule_id: 'saved-query-rule', - tags: ['tagB'], - }, - upgrade: { - rule_id: 'saved-query-rule', - type: 'query', - tags: ['tagC'], - version: 2, - }, - }, - ], - removeInstalledAssets: !withHistoricalVersions, - deps, - }); - - const response = await reviewPrebuiltRulesToUpgrade(supertest); - - expect(response.stats).toMatchObject({ - num_rules_with_conflicts: 2, - }); - }); - - it('returns num of rules with non-solvable conflicts', async () => { - await setUpRuleUpgrade({ - assets: [ - // Name field has a non-solvable upgrade conflict - { - installed: { - rule_id: 'query-rule', - type: 'query', - name: 'Initial name', - version: 1, - }, - patch: { - rule_id: 'query-rule', - name: 'Customized name', - }, - upgrade: { - rule_id: 'query-rule', - type: 'query', - name: 'Updated name', - version: 2, - }, - }, - // tags field values are merged resulting in a solvable upgrade conflict - { - installed: { - rule_id: 'saved-query-rule', - type: 'query', - tags: ['tagA'], - version: 1, - }, - patch: { - rule_id: 'saved-query-rule', - tags: ['tagB'], - }, - upgrade: { - rule_id: 'saved-query-rule', - type: 'query', - tags: ['tagC'], - version: 2, - }, - }, - ], - removeInstalledAssets: !withHistoricalVersions, - deps, - }); - - const response = await reviewPrebuiltRulesToUpgrade(supertest); - - expect(response.stats).toMatchObject({ - // Missing rule's base version doesn't allow to detect non solvable conflicts - num_rules_with_non_solvable_conflicts: withHistoricalVersions ? 1 : 0, - }); - }); - - if (!withHistoricalVersions) { - it('returns num of rules with conflicts caused by missing historical versions', async () => { - await setUpRuleUpgrade({ - assets: [ - { - installed: { - rule_id: 'query-rule', - type: 'query', - name: 'Initial name', - version: 1, - }, - patch: {}, - upgrade: { - rule_id: 'query-rule', - type: 'query', - version: 2, - }, - }, - { - installed: { - rule_id: 'saved-query-rule', - type: 'query', - version: 1, - }, - patch: {}, - upgrade: { - rule_id: 'saved-query-rule', - type: 'query', - name: 'Updated name', - version: 2, - }, - }, - ], - removeInstalledAssets: true, - deps, - }); - - const response = await reviewPrebuiltRulesToUpgrade(supertest); - - expect(response.stats).toMatchObject({ - num_rules_with_conflicts: 2, - }); - }); - } - }); - describe('fields diff stats', () => { it('returns num of fields with updates', async () => { await setUpRuleUpgrade({ diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/update_prebuilt_rules_package.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/update_prebuilt_rules_package.ts index b551d793406ce..6ef7bd9bc514a 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/update_prebuilt_rules_package.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/update_prebuilt_rules_package/trial_license_complete_tier/update_prebuilt_rules_package.ts @@ -21,7 +21,6 @@ import { installPrebuiltRulesPackageByVersion, performUpgradePrebuiltRules, reviewPrebuiltRulesToInstall, - reviewPrebuiltRulesToUpgrade, } from '../../../../utils'; import { deleteAllRules } from '../../../../../../../common/utils/security_solution'; @@ -220,13 +219,6 @@ export default ({ getService }: FtrProviderContext): void => { ) ); - // Verify that the upgrade _review endpoint returns the same number of rules to upgrade as the status endpoint - const prebuiltRulesToUpgradeReviewAfterLatestPackageInstallation = - await reviewPrebuiltRulesToUpgrade(supertest); - expect( - prebuiltRulesToUpgradeReviewAfterLatestPackageInstallation.stats.num_rules_to_upgrade_total - ).toBe(statusAfterLatestPackageInstallation.stats.num_prebuilt_rules_to_upgrade); - // Call the upgrade _perform endpoint to upgrade all rules to their target version and verify that the number // of upgraded rules is the same as the one returned by the _review endpoint and the status endpoint const upgradePrebuiltRulesResponseAfterLatestPackageInstallation = @@ -238,9 +230,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(upgradePrebuiltRulesResponseAfterLatestPackageInstallation.summary.succeeded).toEqual( statusAfterLatestPackageInstallation.stats.num_prebuilt_rules_to_upgrade ); - expect(upgradePrebuiltRulesResponseAfterLatestPackageInstallation.summary.succeeded).toEqual( - prebuiltRulesToUpgradeReviewAfterLatestPackageInstallation.stats.num_rules_to_upgrade_total - ); // Get installed rules diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts index f0f5c9c42a8e8..a8902a3513392 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/install_update_error_handling.cy.ts @@ -5,6 +5,10 @@ * 2.0. */ +import { + deleteAlertsAndRules, + deletePrebuiltRulesAssets, +} from '../../../../tasks/api_calls/common'; import { createRuleAssetSavedObject } from '../../../../helpers/rules'; import { getInstallSingleRuleButtonByRuleId, @@ -42,6 +46,8 @@ describe( { tags: ['@ess', '@serverless', '@skipInServerlessMKI'] }, () => { beforeEach(() => { + deletePrebuiltRulesAssets(); + deleteAlertsAndRules(); preventPrebuiltRulesPackageInstallation(); login(); visitRulesManagementTable(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_customized_rules.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_customized_rules.cy.ts index 36c9d2f851e3b..515ec4e633127 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_customized_rules.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/prebuilt_rules/update_workflow_customized_rules.cy.ts @@ -114,10 +114,6 @@ describe( selectRulesByName(['Old rule 1', 'Old rule 2']); cy.get(UPGRADE_SELECTED_RULES_BUTTON).should('be.disabled'); }); - - it('should disable `Update all rules` button when all rules have conflicts', () => { - cy.get(UPGRADE_ALL_RULES_BUTTON).should('be.disabled'); - }); }); describe('Upgrade of prebuilt rules with and without conflicts', () => { @@ -323,10 +319,6 @@ describe( ]); cy.get(UPGRADE_SELECTED_RULES_BUTTON).should('be.disabled'); }); - - it('should disable `Update all rules` button', () => { - cy.get(UPGRADE_ALL_RULES_BUTTON).should('be.disabled'); - }); }); } ); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts index 08de9decbddf1..2a0b601b2dc60 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/api_calls/prebuilt_rules.ts @@ -145,22 +145,16 @@ export const bulkCreateRuleAssets = ({ const bulkIndexRequestBody = rules.reduce((body, rule) => { const document = JSON.stringify(rule); const documentId = `security-rule:${rule['security-rule'].rule_id}`; - const historicalDocumentId = `${documentId}_${rule['security-rule'].version}`; + const documentIdWithVersion = `${documentId}_${rule['security-rule'].version}`; - const indexRuleAsset = `${JSON.stringify({ - index: { - _index: index, - _id: documentId, - }, - })}\n${document}\n`; const indexHistoricalRuleAsset = `${JSON.stringify({ index: { _index: index, - _id: historicalDocumentId, + _id: documentIdWithVersion, }, })}\n${document}\n`; - return body.concat(indexRuleAsset, indexHistoricalRuleAsset); + return body.concat(indexHistoricalRuleAsset); }, ''); cy.task('putMapping', index); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts index 0dd833810f9be..a7f35548eb262 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/prebuilt_rules.ts @@ -80,6 +80,15 @@ export const interceptUpgradeRequestToFail = (rules: Array 1 ? 'rules' : 'rule'; cy.get(TOASTER) .should('be.visible') - .should('have.text', `${rules.length} ${rulesString} updated successfully.`); + .should('contain', `${rules.length} ${rulesString} updated successfully.`); }; export const assertRuleUpgradeFailureToastShown = (rules: Array) => { From d37fcb6fb686e59f18e541113ef16d01030c8d86 Mon Sep 17 00:00:00 2001 From: Kenneth Kreindler <42113355+KDKHD@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:35:22 +0000 Subject: [PATCH 4/9] [Security Solution] [GenAi] refactor security ai assistant tools to use tool helper method (#212865) ## Summary Clean up some security ai assistant code. - Replace the usage of `new DynamicStructuredTool()` with the `tool()` helper method. This is the recommended approach today and has the correct types to work with [`Command`](https://langchain-ai.github.io/langgraphjs/concepts/low_level/#command). - Extract code such as the default assistant graph state and agentRunnableFactory to reduce cognitive overload. - Update AssistantTool type definition ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [X] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [X] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [X] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [X] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [X] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] [See some risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) - [ ] ... --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../default_assistant_graph/agentRunnable.ts | 56 ++++++++++++ .../graphs/default_assistant_graph/graph.ts | 83 ++---------------- .../graphs/default_assistant_graph/index.ts | 48 ++++------- .../graphs/default_assistant_graph/prompts.ts | 22 ++++- .../graphs/default_assistant_graph/state.ts | 86 +++++++++++++++++++ .../server/routes/evaluate/post_evaluate.ts | 48 ++++------- .../plugins/elastic_assistant/server/types.ts | 4 +- .../tools/alert_counts/alert_counts_tool.ts | 18 ++-- .../assistant/tools/esql/nl_to_esql_tool.ts | 29 ++++--- .../knowledge_base_retrieval_tool.ts | 23 ++--- .../knowledge_base_write_tool.ts | 41 ++++----- .../open_and_acknowledged_alerts_tool.ts | 18 ++-- .../product_documentation_tool.ts | 59 ++++++------- .../tools/security_labs/security_labs_tool.ts | 31 +++---- 14 files changed, 318 insertions(+), 248 deletions(-) create mode 100644 x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/agentRunnable.ts create mode 100644 x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/state.ts diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/agentRunnable.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/agentRunnable.ts new file mode 100644 index 0000000000000..8fc4faa371d3f --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/agentRunnable.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ToolDefinition } from '@langchain/core/language_models/base'; +import { + ActionsClientChatBedrockConverse, + ActionsClientChatVertexAI, + ActionsClientChatOpenAI, +} from '@kbn/langchain/server'; +import type { StructuredToolInterface } from '@langchain/core/tools'; +import { + AgentRunnableSequence, + createOpenAIToolsAgent, + createStructuredChatAgent, + createToolCallingAgent, +} from 'langchain/agents'; +import { ChatPromptTemplate } from '@langchain/core/prompts'; + +export const TOOL_CALLING_LLM_TYPES = new Set(['bedrock', 'gemini']); + +export const agentRunableFactory = async ({ + llm, + isOpenAI, + llmType, + tools, + isStream, + prompt, +}: { + llm: ActionsClientChatBedrockConverse | ActionsClientChatVertexAI | ActionsClientChatOpenAI; + isOpenAI: boolean; + llmType: string | undefined; + tools: StructuredToolInterface[] | ToolDefinition[]; + isStream: boolean; + prompt: ChatPromptTemplate; +}): Promise => { + const params = { + llm, + tools, + streamRunnable: isStream, + prompt, + } as const; + + if (isOpenAI || llmType === 'inference') { + return createOpenAIToolsAgent(params); + } + + if (llmType && TOOL_CALLING_LLM_TYPES.has(llmType)) { + return createToolCallingAgent(params); + } + + return createStructuredChatAgent(params); +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts index 8e85b20b06c8a..7f8502cf4b4c7 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/graph.ts @@ -5,15 +5,13 @@ * 2.0. */ -import { Annotation, END, START, StateGraph } from '@langchain/langgraph'; -import { AgentAction, AgentFinish, AgentStep } from '@langchain/core/agents'; +import { END, START, StateGraph } from '@langchain/langgraph'; import { AgentRunnableSequence } from 'langchain/dist/agents/agent'; import { StructuredTool } from '@langchain/core/tools'; import type { Logger } from '@kbn/logging'; -import { BaseMessage } from '@langchain/core/messages'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { ConversationResponse, Replacements } from '@kbn/elastic-assistant-common'; +import { Replacements } from '@kbn/elastic-assistant-common'; import { PublicMethodsOf } from '@kbn/utility-types'; import { ActionsClient } from '@kbn/actions-plugin/server'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; @@ -29,6 +27,7 @@ import { getPersistedConversation } from './nodes/get_persisted_conversation'; import { persistConversationChanges } from './nodes/persist_conversation_changes'; import { respond } from './nodes/respond'; import { NodeType } from './constants'; +import { getStateAnnotation } from './state'; export const DEFAULT_ASSISTANT_GRAPH_ID = 'Default Security Assistant Graph'; @@ -61,78 +60,6 @@ export const getDefaultAssistantGraph = ({ getFormattedTime, }: GetDefaultAssistantGraphParams) => { try { - // Default graph state - const graphAnnotation = Annotation.Root({ - input: Annotation({ - reducer: (x: string, y?: string) => y ?? x, - default: () => '', - }), - lastNode: Annotation({ - reducer: (x: string, y?: string) => y ?? x, - default: () => 'start', - }), - steps: Annotation({ - reducer: (x: AgentStep[], y: AgentStep[]) => x.concat(y), - default: () => [], - }), - hasRespondStep: Annotation({ - reducer: (x: boolean, y?: boolean) => y ?? x, - default: () => false, - }), - agentOutcome: Annotation({ - reducer: ( - x: AgentAction | AgentFinish | undefined, - y?: AgentAction | AgentFinish | undefined - ) => y ?? x, - default: () => undefined, - }), - messages: Annotation({ - reducer: (x: BaseMessage[], y: BaseMessage[]) => y ?? x, - default: () => [], - }), - chatTitle: Annotation({ - reducer: (x: string, y?: string) => y ?? x, - default: () => '', - }), - llmType: Annotation({ - reducer: (x: string, y?: string) => y ?? x, - default: () => 'unknown', - }), - isStream: Annotation({ - reducer: (x: boolean, y?: boolean) => y ?? x, - default: () => false, - }), - isOssModel: Annotation({ - reducer: (x: boolean, y?: boolean) => y ?? x, - default: () => false, - }), - connectorId: Annotation({ - reducer: (x: string, y?: string) => y ?? x, - default: () => '', - }), - conversation: Annotation({ - reducer: (x: ConversationResponse | undefined, y?: ConversationResponse | undefined) => - y ?? x, - default: () => undefined, - }), - conversationId: Annotation({ - reducer: (x: string, y?: string) => y ?? x, - default: () => '', - }), - responseLanguage: Annotation({ - reducer: (x: string, y?: string) => y ?? x, - default: () => 'English', - }), - provider: Annotation({ - reducer: (x: string, y?: string) => y ?? x, - default: () => '', - }), - formattedTime: Annotation({ - reducer: (x: string, y?: string) => y ?? x, - default: getFormattedTime ?? (() => ''), - }), - }); - // Default node parameters const nodeParams: NodeParamsBase = { actionsClient, @@ -140,8 +67,10 @@ export const getDefaultAssistantGraph = ({ savedObjectsClient, }; + const stateAnnotation = getStateAnnotation({ getFormattedTime }); + // Put together a new graph using default state from above - const graph = new StateGraph(graphAnnotation) + const graph = new StateGraph(stateAnnotation) .addNode(NodeType.GET_PERSISTED_CONVERSATION, (state: AgentState) => getPersistedConversation({ ...nodeParams, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts index 3bfd41329ebae..da1d2244e5c5e 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/index.ts @@ -7,11 +7,6 @@ import { StructuredTool } from '@langchain/core/tools'; import { getDefaultArguments } from '@kbn/langchain/server'; -import { - createOpenAIToolsAgent, - createStructuredChatAgent, - createToolCallingAgent, -} from 'langchain/agents'; import { APMTracer } from '@kbn/langchain/server/tracers/apm'; import { TelemetryTracer } from '@kbn/langchain/server/tracers/telemetry'; import { pruneContentReferences, MessageMetadata } from '@kbn/elastic-assistant-common'; @@ -25,12 +20,13 @@ import { getLlmClass } from '../../../../routes/utils'; import { EsAnonymizationFieldsSchema } from '../../../../ai_assistant_data_clients/anonymization_fields/types'; import { AssistantToolParams } from '../../../../types'; import { AgentExecutor } from '../../executors/types'; -import { formatPrompt, formatPromptStructured } from './prompts'; +import { formatPrompt } from './prompts'; import { GraphInputs } from './types'; import { getDefaultAssistantGraph } from './graph'; import { invokeGraph, streamGraph } from './helpers'; import { transformESSearchToAnonymizationFields } from '../../../../ai_assistant_data_clients/anonymization_fields/helpers'; import { DEFAULT_DATE_FORMAT_TZ } from '../../../../../common/constants'; +import { agentRunableFactory } from './agentRunnable'; export const callAssistantGraph: AgentExecutor = async ({ abortSignal, @@ -179,28 +175,21 @@ export const callAssistantGraph: AgentExecutor = async ({ savedObjectsClient, }); - const agentRunnable = - isOpenAI || llmType === 'inference' - ? await createOpenAIToolsAgent({ - llm: createLlmInstance(), - tools, - prompt: formatPrompt(defaultSystemPrompt, systemPrompt), - streamRunnable: isStream, - }) - : llmType && ['bedrock', 'gemini'].includes(llmType) - ? await createToolCallingAgent({ - llm: createLlmInstance(), - tools, - prompt: formatPrompt(defaultSystemPrompt, systemPrompt), - streamRunnable: isStream, - }) - : // used with OSS models - await createStructuredChatAgent({ - llm: createLlmInstance(), - tools, - prompt: formatPromptStructured(defaultSystemPrompt, systemPrompt), - streamRunnable: isStream, - }); + const chatPromptTemplate = formatPrompt({ + prompt: defaultSystemPrompt, + additionalPrompt: systemPrompt, + llmType, + isOpenAI, + }); + + const agentRunnable = await agentRunableFactory({ + llm: createLlmInstance(), + isOpenAI, + llmType, + tools, + isStream, + prompt: chatPromptTemplate, + }); const apmTracer = new APMTracer({ projectName: traceOptions?.projectName ?? 'default' }, logger); const telemetryTracer = telemetryParams @@ -214,6 +203,7 @@ export const callAssistantGraph: AgentExecutor = async ({ logger ) : undefined; + const { provider } = !llmType || llmType === 'inference' ? await resolveProviderAndModel({ @@ -240,7 +230,7 @@ export const callAssistantGraph: AgentExecutor = async ({ ...(llmType === 'bedrock' ? { signal: abortSignal } : {}), getFormattedTime: () => getFormattedTime({ - screenContextTimezone: request.body.screenContext?.timeZone, + screenContextTimezone: screenContext?.timeZone, uiSettingsDateFormatTimezone, }), }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts index bc28f00e5d76e..79327648dde34 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts @@ -6,8 +6,9 @@ */ import { ChatPromptTemplate } from '@langchain/core/prompts'; +import { TOOL_CALLING_LLM_TYPES } from './agentRunnable'; -export const formatPrompt = (prompt: string, additionalPrompt?: string) => +const formatPromptToolcalling = (prompt: string, additionalPrompt?: string) => ChatPromptTemplate.fromMessages([ ['system', additionalPrompt ? `${prompt}\n\n${additionalPrompt}` : prompt], ['placeholder', '{knowledge_history}'], @@ -16,7 +17,7 @@ export const formatPrompt = (prompt: string, additionalPrompt?: string) => ['placeholder', '{agent_scratchpad}'], ]); -export const formatPromptStructured = (prompt: string, additionalPrompt?: string) => +const formatPromptStructured = (prompt: string, additionalPrompt?: string) => ChatPromptTemplate.fromMessages([ ['system', additionalPrompt ? `${prompt}\n\n${additionalPrompt}` : prompt], ['placeholder', '{knowledge_history}'], @@ -26,3 +27,20 @@ export const formatPromptStructured = (prompt: string, additionalPrompt?: string '{input}\n\n{agent_scratchpad}\n\n(reminder to respond in a JSON blob no matter what)', ], ]); + +export const formatPrompt = ({ + isOpenAI, + llmType, + prompt, + additionalPrompt, +}: { + isOpenAI: boolean; + llmType: string | undefined; + prompt: string; + additionalPrompt?: string; +}) => { + if (isOpenAI || llmType === 'inference' || (llmType && TOOL_CALLING_LLM_TYPES.has(llmType))) { + return formatPromptToolcalling(prompt, additionalPrompt); + } + return formatPromptStructured(prompt, additionalPrompt); +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/state.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/state.ts new file mode 100644 index 0000000000000..f1ab308adfefb --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/state.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConversationResponse } from '@kbn/elastic-assistant-common'; +import { BaseMessage } from '@langchain/core/messages'; +import { Annotation } from '@langchain/langgraph'; +import { AgentStep, AgentAction, AgentFinish } from 'langchain/agents'; + +export const getStateAnnotation = ({ getFormattedTime }: { getFormattedTime?: () => string }) => { + const graphAnnotation = Annotation.Root({ + input: Annotation({ + reducer: (x: string, y?: string) => y ?? x, + default: () => '', + }), + lastNode: Annotation({ + reducer: (x: string, y?: string) => y ?? x, + default: () => 'start', + }), + steps: Annotation({ + reducer: (x: AgentStep[], y: AgentStep[]) => x.concat(y), + default: () => [], + }), + hasRespondStep: Annotation({ + reducer: (x: boolean, y?: boolean) => y ?? x, + default: () => false, + }), + agentOutcome: Annotation({ + reducer: ( + x: AgentAction | AgentFinish | undefined, + y?: AgentAction | AgentFinish | undefined + ) => y ?? x, + default: () => undefined, + }), + messages: Annotation({ + reducer: (x: BaseMessage[], y: BaseMessage[]) => y ?? x, + default: () => [], + }), + chatTitle: Annotation({ + reducer: (x: string, y?: string) => y ?? x, + default: () => '', + }), + llmType: Annotation({ + reducer: (x: string, y?: string) => y ?? x, + default: () => 'unknown', + }), + isStream: Annotation({ + reducer: (x: boolean, y?: boolean) => y ?? x, + default: () => false, + }), + isOssModel: Annotation({ + reducer: (x: boolean, y?: boolean) => y ?? x, + default: () => false, + }), + connectorId: Annotation({ + reducer: (x: string, y?: string) => y ?? x, + default: () => '', + }), + conversation: Annotation({ + reducer: (x: ConversationResponse | undefined, y?: ConversationResponse | undefined) => + y ?? x, + default: () => undefined, + }), + conversationId: Annotation({ + reducer: (x: string, y?: string) => y ?? x, + default: () => '', + }), + responseLanguage: Annotation({ + reducer: (x: string, y?: string) => y ?? x, + default: () => 'English', + }), + provider: Annotation({ + reducer: (x: string, y?: string) => y ?? x, + default: () => '', + }), + formattedTime: Annotation({ + reducer: (x: string, y?: string) => y ?? x, + default: getFormattedTime ?? (() => ''), + }), + }); + + return graphAnnotation; +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts index bb440f034605a..ae82fec6ceeca 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts @@ -26,21 +26,13 @@ import { import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common'; import { getDefaultArguments } from '@kbn/langchain/server'; import { StructuredTool } from '@langchain/core/tools'; -import { - AgentFinish, - createOpenAIToolsAgent, - createStructuredChatAgent, - createToolCallingAgent, -} from 'langchain/agents'; +import { AgentFinish } from 'langchain/agents'; import { omit } from 'lodash/fp'; import { localToolPrompts, promptGroupId as toolsGroupId } from '../../lib/prompt/tool_prompts'; import { promptGroupId } from '../../lib/prompt/local_prompt_object'; import { getFormattedTime, getModelOrOss } from '../../lib/prompt/helpers'; import { getAttackDiscoveryPrompts } from '../../lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/prompts'; -import { - formatPrompt, - formatPromptStructured, -} from '../../lib/langchain/graphs/default_assistant_graph/prompts'; +import { formatPrompt } from '../../lib/langchain/graphs/default_assistant_graph/prompts'; import { getPrompt as localGetPrompt, promptDictionary } from '../../lib/prompt'; import { buildResponse } from '../../lib/build_response'; import { AssistantDataClients } from '../../lib/langchain/executors/types'; @@ -57,6 +49,7 @@ import { import { getLlmClass, getLlmType, isOpenSourceModel } from '../utils'; import { getGraphsFromNames } from './get_graphs_from_names'; import { DEFAULT_DATE_FORMAT_TZ } from '../../../common/constants'; +import { agentRunableFactory } from '../../lib/langchain/graphs/default_assistant_graph/agentRunnable'; const DEFAULT_SIZE = 20; const ROUTE_HANDLER_TIMEOUT = 10 * 60 * 1000; // 10 * 60 seconds = 10 minutes @@ -356,27 +349,20 @@ export const postEvaluateRoute = ( savedObjectsClient, }); - const agentRunnable = - isOpenAI || llmType === 'inference' - ? await createOpenAIToolsAgent({ - llm, - tools, - prompt: formatPrompt(defaultSystemPrompt), - streamRunnable: false, - }) - : llmType && ['bedrock', 'gemini'].includes(llmType) - ? createToolCallingAgent({ - llm, - tools, - prompt: formatPrompt(defaultSystemPrompt), - streamRunnable: false, - }) - : await createStructuredChatAgent({ - llm, - tools, - prompt: formatPromptStructured(defaultSystemPrompt), - streamRunnable: false, - }); + const chatPromptTemplate = formatPrompt({ + prompt: defaultSystemPrompt, + llmType, + isOpenAI, + }); + + const agentRunnable = await agentRunableFactory({ + llm: createLlmInstance(), + isOpenAI, + llmType, + tools, + isStream: false, + prompt: chatPromptTemplate, + }); const uiSettingsDateFormatTimezone = await ctx.core.uiSettings.client.get( DEFAULT_DATE_FORMAT_TZ diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts index 5730336d17b31..1dfe451a915f8 100755 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts @@ -24,7 +24,7 @@ import { } from '@kbn/core/server'; import type { LlmTasksPluginStart } from '@kbn/llm-tasks-plugin/server'; import { type MlPluginSetup } from '@kbn/ml-plugin/server'; -import { DynamicStructuredTool, Tool } from '@langchain/core/tools'; +import { StructuredToolInterface } from '@langchain/core/tools'; import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server'; import { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; import { @@ -224,7 +224,7 @@ export interface AssistantTool { description: string; sourceRegister: string; isSupported: (params: AssistantToolParams) => boolean; - getTool: (params: AssistantToolParams) => Tool | DynamicStructuredTool | null; + getTool: (params: AssistantToolParams) => StructuredToolInterface | null; } export type AssistantToolLlm = diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/alert_counts/alert_counts_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/alert_counts/alert_counts_tool.ts index 801b4054a8ef3..cc36d029f1c2e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/alert_counts/alert_counts_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/alert_counts/alert_counts_tool.ts @@ -6,8 +6,7 @@ */ import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; -import { DynamicStructuredTool } from '@langchain/core/tools'; -import { z } from '@kbn/zod'; +import { tool } from '@langchain/core/tools'; import { requestHasRequiredAnonymizationParams } from '@kbn/elastic-assistant-plugin/server/lib/langchain/helpers'; import type { AssistantTool, AssistantToolParams } from '@kbn/elastic-assistant-plugin/server'; import { contentReferenceString, securityAlertsPageReference } from '@kbn/elastic-assistant-common'; @@ -36,11 +35,8 @@ export const ALERT_COUNTS_TOOL: AssistantTool = { if (!this.isSupported(params)) return null; const { alertsIndexPattern, esClient, contentReferencesStore } = params as AlertCountsToolParams; - return new DynamicStructuredTool({ - name: 'AlertCountsTool', - description: params.description || ALERT_COUNTS_TOOL_DESCRIPTION, - schema: z.object({}), - func: async () => { + return tool( + async () => { const query = getAlertsCountQuery(alertsIndexPattern); const result = await esClient.search(query); const alertsCountReference = contentReferencesStore?.add((p) => @@ -51,7 +47,11 @@ export const ALERT_COUNTS_TOOL: AssistantTool = { return `${JSON.stringify(result)}${reference}`; }, - tags: ['alerts', 'alerts-count'], - }); + { + name: 'AlertCountsTool', + description: params.description || ALERT_COUNTS_TOOL_DESCRIPTION, + tags: ['alerts', 'alerts-count'], + } + ); }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.ts index 0d2af41232f70..33a4286c020b1 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/esql/nl_to_esql_tool.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { DynamicStructuredTool } from '@langchain/core/tools'; -import { z } from '@kbn/zod'; +import { tool } from '@langchain/core/tools'; import type { AssistantTool, AssistantToolParams } from '@kbn/elastic-assistant-plugin/server'; import { lastValueFrom } from 'rxjs'; import { naturalLanguageToEsql } from '@kbn/inference-plugin/server'; +import { z } from '@kbn/zod'; import { APP_UI_ID } from '../../../../common'; import { getPromptSuffixForOssModel } from './common'; @@ -57,23 +57,24 @@ export const NL_TO_ESQL_TOOL: AssistantTool = { ); }; - return new DynamicStructuredTool({ - name: toolDetails.name, - description: - (params.description || toolDetails.description) + - (isOssModel ? getPromptSuffixForOssModel(TOOL_NAME) : ''), - schema: z.object({ - question: z.string().describe(`The user's exact question about ESQL`), - }), - func: async (input) => { + return tool( + async (input) => { const generateEvent = await callNaturalLanguageToEsql(input.question); const answer = generateEvent.content ?? 'An error occurred in the tool'; logger.debug(`Received response from NL to ESQL tool: ${answer}`); return answer; }, - tags: ['esql', 'query-generation', 'knowledge-base'], - // TODO: Remove after ZodAny is fixed https://github.com/langchain-ai/langchainjs/blob/main/langchain-core/src/tools.ts - }) as unknown as DynamicStructuredTool; + { + name: toolDetails.name, + description: + (params.description || toolDetails.description) + + (isOssModel ? getPromptSuffixForOssModel(TOOL_NAME) : ''), + schema: z.object({ + question: z.string().describe(`The user's exact question about ESQL`), + }), + tags: ['esql', 'query-generation', 'knowledge-base'], + } + ); }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_retrieval_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_retrieval_tool.ts index beddd4efeadb9..6cff2ccb63722 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_retrieval_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_retrieval_tool.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DynamicStructuredTool } from '@langchain/core/tools'; +import { tool } from '@langchain/core/tools'; import { z } from '@kbn/zod'; import type { AssistantTool, AssistantToolParams } from '@kbn/elastic-assistant-plugin/server'; import type { AIAssistantKnowledgeBaseDataClient } from '@kbn/elastic-assistant-plugin/server/ai_assistant_data_clients/knowledge_base'; @@ -41,13 +41,8 @@ export const KNOWLEDGE_BASE_RETRIEVAL_TOOL: AssistantTool = { params as KnowledgeBaseRetrievalToolParams; if (kbDataClient == null) return null; - return new DynamicStructuredTool({ - name: toolDetails.name, - description: params.description || toolDetails.description, - schema: z.object({ - query: z.string().describe(`Summary of items/things to search for in the knowledge base`), - }), - func: async (input) => { + return tool( + async (input) => { logger.debug( () => `KnowledgeBaseRetrievalToolParams:input\n ${JSON.stringify(input, null, 2)}` ); @@ -60,9 +55,15 @@ export const KNOWLEDGE_BASE_RETRIEVAL_TOOL: AssistantTool = { return JSON.stringify(docs.map(enrichDocument(contentReferencesStore))); }, - tags: ['knowledge-base'], - // TODO: Remove after ZodAny is fixed https://github.com/langchain-ai/langchainjs/blob/main/langchain-core/src/tools.ts - }) as unknown as DynamicStructuredTool; + { + name: toolDetails.name, + description: params.description || toolDetails.description, + schema: z.object({ + query: z.string().describe(`Summary of items/things to search for in the knowledge base`), + }), + tags: ['knowledge-base'], + } + ); }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_write_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_write_tool.ts index d3fb2110e7c79..abaca93e43ada 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_write_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/knowledge_base/knowledge_base_write_tool.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DynamicStructuredTool } from '@langchain/core/tools'; +import { tool } from '@langchain/core/tools'; import { z } from '@kbn/zod'; import type { AssistantTool, AssistantToolParams } from '@kbn/elastic-assistant-plugin/server'; import type { AIAssistantKnowledgeBaseDataClient } from '@kbn/elastic-assistant-plugin/server/ai_assistant_data_clients/knowledge_base'; @@ -41,22 +41,8 @@ export const KNOWLEDGE_BASE_WRITE_TOOL: AssistantTool = { const { telemetry, kbDataClient, logger } = params as KnowledgeBaseWriteToolParams; if (kbDataClient == null) return null; - return new DynamicStructuredTool({ - name: toolDetails.name, - description: params.description || toolDetails.description, - schema: z.object({ - name: z - .string() - .describe(`This is what the user will use to refer to the entry in the future.`), - query: z.string().describe(`Summary of items/things to save in the knowledge base`), - required: z - .boolean() - .describe( - `Whether or not the entry is required to always be included in conversations. Is only true if the user explicitly asks for it to be required or always included in conversations, otherwise this is always false.` - ) - .default(false), - }), - func: async (input) => { + return tool( + async (input) => { logger.debug( () => `KnowledgeBaseWriteToolParams:input\n ${JSON.stringify(input, null, 2)}` ); @@ -78,8 +64,23 @@ export const KNOWLEDGE_BASE_WRITE_TOOL: AssistantTool = { } return "I've successfully saved this entry to your knowledge base. You can ask me to recall this information at any time."; }, - tags: ['knowledge-base'], - // TODO: Remove after ZodAny is fixed https://github.com/langchain-ai/langchainjs/blob/main/langchain-core/src/tools.ts - }) as unknown as DynamicStructuredTool; + { + name: toolDetails.name, + description: params.description || toolDetails.description, + schema: z.object({ + name: z + .string() + .describe(`This is what the user will use to refer to the entry in the future.`), + query: z.string().describe(`Summary of items/things to save in the knowledge base`), + required: z + .boolean() + .describe( + `Whether or not the entry is required to always be included in conversations. Is only true if the user explicitly asks for it to be required or always included in conversations, otherwise this is always false.` + ) + .default(false), + }), + tags: ['knowledge-base'], + } + ); }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.ts index d73bc266239d5..835c6a8d38d0c 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.ts @@ -16,9 +16,8 @@ import { transformRawData, contentReferenceBlock, } from '@kbn/elastic-assistant-common'; -import { DynamicStructuredTool } from '@langchain/core/tools'; +import { tool } from '@langchain/core/tools'; import { requestHasRequiredAnonymizationParams } from '@kbn/elastic-assistant-plugin/server/lib/langchain/helpers'; -import { z } from '@kbn/zod'; import type { AssistantTool, AssistantToolParams } from '@kbn/elastic-assistant-plugin/server'; import { APP_UI_ID } from '../../../../common'; @@ -63,11 +62,8 @@ export const OPEN_AND_ACKNOWLEDGED_ALERTS_TOOL: AssistantTool = { size, contentReferencesStore, } = params as OpenAndAcknowledgedAlertsToolParams; - return new DynamicStructuredTool({ - name: 'OpenAndAcknowledgedAlertsTool', - description: params.description || OPEN_AND_ACKNOWLEDGED_ALERTS_TOOL_DESCRIPTION, - schema: z.object({}), - func: async () => { + return tool( + async () => { const query = getOpenAndAcknowledgedAlertsQuery({ alertsIndexPattern, anonymizationFields: anonymizationFields ?? [], @@ -105,7 +101,11 @@ export const OPEN_AND_ACKNOWLEDGED_ALERTS_TOOL: AssistantTool = { }) ); }, - tags: ['alerts', 'open-and-acknowledged-alerts'], - }); + { + name: 'OpenAndAcknowledgedAlertsTool', + description: params.description || OPEN_AND_ACKNOWLEDGED_ALERTS_TOOL_DESCRIPTION, + tags: ['alerts', 'open-and-acknowledged-alerts'], + } + ); }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/product_docs/product_documentation_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/product_docs/product_documentation_tool.ts index 30dd2a1ec50cb..014be943cca3e 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/product_docs/product_documentation_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/product_docs/product_documentation_tool.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DynamicStructuredTool } from '@langchain/core/tools'; +import { tool } from '@langchain/core/tools'; import { z } from '@kbn/zod'; import type { AssistantTool, AssistantToolParams } from '@kbn/elastic-assistant-plugin/server'; @@ -41,31 +41,8 @@ export const PRODUCT_DOCUMENTATION_TOOL: AssistantTool = { // This check is here in order to satisfy TypeScript if (llmTasks == null || connectorId == null) return null; - return new DynamicStructuredTool({ - name: toolDetails.name, - description: params.description || toolDetails.description, - schema: z.object({ - query: z.string().describe( - `The query to use to retrieve documentation - Examples: - - "How to enable TLS for Elasticsearch?" - - "What is Kibana Security?"` - ), - product: z - .enum(['kibana', 'elasticsearch', 'observability', 'security']) - .describe( - `If specified, will filter the products to retrieve documentation for - Possible options are: - - "kibana": Kibana product - - "elasticsearch": Elasticsearch product - - "observability": Elastic Observability solution - - "security": Elastic Security solution - If not specified, will search against all products - ` - ) - .optional(), - }), - func: async ({ query, product }) => { + return tool( + async ({ query, product }) => { const response = await llmTasks.retrieveDocumentation({ searchTerm: query, products: product ? [product] : undefined, @@ -83,9 +60,33 @@ export const PRODUCT_DOCUMENTATION_TOOL: AssistantTool = { }, }; }, - tags: ['product-documentation'], - // TODO: Remove after ZodAny is fixed https://github.com/langchain-ai/langchainjs/blob/main/langchain-core/src/tools.ts - }) as unknown as DynamicStructuredTool; + { + name: toolDetails.name, + description: params.description || toolDetails.description, + schema: z.object({ + query: z.string().describe( + `The query to use to retrieve documentation + Examples: + - "How to enable TLS for Elasticsearch?" + - "What is Kibana Security?"` + ), + product: z + .enum(['kibana', 'elasticsearch', 'observability', 'security']) + .describe( + `If specified, will filter the products to retrieve documentation for + Possible options are: + - "kibana": Kibana product + - "elasticsearch": Elasticsearch product + - "observability": Elastic Observability solution + - "security": Elastic Security solution + If not specified, will search against all products + ` + ) + .optional(), + }), + tags: ['product-documentation'], + } + ); }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts index 2faad9ba71c06..61c7c1dc82299 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/assistant/tools/security_labs/security_labs_tool.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DynamicStructuredTool } from '@langchain/core/tools'; +import { tool } from '@langchain/core/tools'; import { z } from '@kbn/zod'; import type { AssistantTool, AssistantToolParams } from '@kbn/elastic-assistant-plugin/server'; @@ -35,17 +35,8 @@ export const SECURITY_LABS_KNOWLEDGE_BASE_TOOL: AssistantTool = { const { kbDataClient, contentReferencesStore } = params as AssistantToolParams; if (kbDataClient == null) return null; - return new DynamicStructuredTool({ - name: toolDetails.name, - description: params.description || toolDetails.description, - schema: z.object({ - question: z - .string() - .describe( - `Key terms to retrieve Elastic Security Labs content for, like specific malware names or attack techniques.` - ), - }), - func: async (input) => { + return tool( + async (input) => { const docs = await kbDataClient.getKnowledgeBaseDocumentEntries({ kbResource: SECURITY_LABS_RESOURCE, query: input.question, @@ -61,8 +52,18 @@ export const SECURITY_LABS_KNOWLEDGE_BASE_TOOL: AssistantTool = { const citation = contentReferenceString(reference); return `${result}\n${citation}`; }, - tags: ['security-labs', 'knowledge-base'], - // TODO: Remove after ZodAny is fixed https://github.com/langchain-ai/langchainjs/blob/main/langchain-core/src/tools.ts - }) as unknown as DynamicStructuredTool; + { + name: toolDetails.name, + description: params.description || toolDetails.description, + schema: z.object({ + question: z + .string() + .describe( + `Key terms to retrieve Elastic Security Labs content for, like specific malware names or attack techniques.` + ), + }), + tags: ['security-labs', 'knowledge-base'], + } + ); }, }; From ed30926f0f402409eb03eae06c9ebd82504f5dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Bl=C3=A1zquez?= Date: Mon, 3 Mar 2025 15:52:04 +0100 Subject: [PATCH 5/9] Remove page and links to Cloud Defend from Assets (#212753) ## Summary Closes: - https://github.com/elastic/security-team/issues/11933. Continues work on: - https://github.com/elastic/kibana/pull/200895. ### Acceptance criteria - `9.0` / `Serverless`: - Removes links to Cloud Defend from Assets page in Security Solution. - Disables navigation to `app/security/cloud_defend/` redirecting to the default `app/security/get_started/`. - `8.x` / `8.18`: - No changes (impact is minimal, only affects 4 customers who were told to uninstall the plugin) ### Screenshot
Before - Assets page Screenshot 2025-02-27 at 19 35 38
Before - Cloud Defend page Screenshot 2025-02-27 at 19 36 57
After - Assets page Screenshot 2025-02-28 at 12 12 11
After - Cloud Defend page redirects to get_started/siem_migrations Screenshot 2025-02-28 at 11 25 43
### How to test Authenticate to Docker Registry with ```bash docker login -u -p docker.elastic.co ``` Then run ES with ```bash yarn es serverless --projectType security --kill ``` In a second terminal, run Kibana with ```bash yarn serverless-security ``` ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks We should not show more links to Cloud Defend in other parts of the app because the feature was deprecated and it might confuse end users. But there's no risk of breaking the app because navigation is handled correctly. --------- Co-authored-by: Elastic Machine --- .../shared/deeplinks/security/deep_links.ts | 5 -- .../translations/translations/fr-FR.json | 3 -- .../translations/translations/ja-JP.json | 3 -- .../translations/translations/zh-CN.json | 3 -- .../public/common/navigation/constants.ts | 2 +- .../plugins/cloud_defend/public/index.ts | 1 - .../security_solution/common/constants.ts | 1 - .../links/sections/assets_links.ts | 19 +------ .../links/sections/assets_translations.ts | 13 ----- .../links/sections/icons/ecctl.tsx | 41 --------------- .../links/sections/lazy_icons.tsx | 1 - .../public/cloud_defend/index.ts | 3 +- .../public/cloud_defend/links.ts | 27 ---------- .../public/cloud_defend/routes.tsx | 51 ------------------- .../public/common/icons/cloud_defend.tsx | 50 ------------------ .../public/management/links.ts | 2 - .../e2e/explore/navigation/navigation.cy.ts | 5 -- .../screens/serverless_security_header.ts | 4 +- .../cypress/urls/navigation.ts | 1 - 19 files changed, 5 insertions(+), 230 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/icons/ecctl.tsx delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/cloud_defend/links.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/cloud_defend/routes.tsx delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/common/icons/cloud_defend.tsx diff --git a/src/platform/packages/shared/deeplinks/security/deep_links.ts b/src/platform/packages/shared/deeplinks/security/deep_links.ts index bf312230d4cf1..e03f659378edf 100644 --- a/src/platform/packages/shared/deeplinks/security/deep_links.ts +++ b/src/platform/packages/shared/deeplinks/security/deep_links.ts @@ -29,11 +29,6 @@ export enum SecurityPageName { cloudSecurityPostureDashboard = 'cloud_security_posture-dashboard', cloudSecurityPostureFindings = 'cloud_security_posture-findings', cloudSecurityPostureRules = 'cloud_security_posture-rules', - /* - * Warning: Computed values are not permitted in an enum with string valued members - * All cloud defend page names must match `CloudDefendPageId` in x-pack/solutions/security/plugins/cloud_defend/public/common/navigation/types.ts - */ - cloudDefend = 'cloud_defend', cloudDefendPolicies = 'cloud_defend-policies', dashboards = 'dashboards', dataQuality = 'data_quality', diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index d1b3412d7f664..d6d51b6782264 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -33936,7 +33936,6 @@ "xpack.securitySolution.appLinks.category.entityAnalytics": "Analyse des entités", "xpack.securitySolution.appLinks.category.investigations": "Investigations", "xpack.securitySolution.appLinks.category.management": "Gestion", - "xpack.securitySolution.appLinks.cloudDefendPoliciesDescription": "Sécurisez les charges de travail conteneurisées dans Kubernetes contre les attaques et les dérives grâce à des politiques d'exécution granulaires et flexibles.", "xpack.securitySolution.appLinks.cloudSecurityPostureBenchmarksDescription": "Voir les règles de benchmark pour la gestion du niveau de sécurité du cloud.", "xpack.securitySolution.appLinks.cloudSecurityPostureDashboardDescription": "Un aperçu des résultats de toutes les intégrations CSP.", "xpack.securitySolution.appLinks.coverageOverviewDashboard": "Couverture MITRE ATT&CK", @@ -38327,8 +38326,6 @@ "xpack.securitySolution.navigation.rules": "Règles", "xpack.securitySolution.navigation.timelines": "Chronologies", "xpack.securitySolution.navigation.users": "Utilisateurs", - "xpack.securitySolution.navLinks.assets.cloud_defend.description": "Hôtes du cloud exécutant Elastic Defend", - "xpack.securitySolution.navLinks.assets.cloud_defend.title": "Cloud", "xpack.securitySolution.navLinks.assets.fleet.agents.title": "Agents", "xpack.securitySolution.navLinks.assets.fleet.dataStreams.title": "Flux de données", "xpack.securitySolution.navLinks.assets.fleet.description": "Gestion centralisée des agents Elastic Agent", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 18c6edbdf1789..1ed63c0b14cbf 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -33798,7 +33798,6 @@ "xpack.securitySolution.appLinks.category.entityAnalytics": "エンティティ分析", "xpack.securitySolution.appLinks.category.investigations": "調査", "xpack.securitySolution.appLinks.category.management": "管理", - "xpack.securitySolution.appLinks.cloudDefendPoliciesDescription": "粒度の高い柔軟なランタイムポリシーによって、Kubernetesのコンテナーワークロードを攻撃とドリフトから保護します。", "xpack.securitySolution.appLinks.cloudSecurityPostureBenchmarksDescription": "Cloud Security Posture Managementのベンチマークルールを表示します。", "xpack.securitySolution.appLinks.cloudSecurityPostureDashboardDescription": "すべてのCSP統合の結果の概要。", "xpack.securitySolution.appLinks.coverageOverviewDashboard": "MITRE ATT&CKの範囲", @@ -38188,8 +38187,6 @@ "xpack.securitySolution.navigation.rules": "ルール", "xpack.securitySolution.navigation.timelines": "タイムライン", "xpack.securitySolution.navigation.users": "ユーザー", - "xpack.securitySolution.navLinks.assets.cloud_defend.description": "Elastic Defendを実行しているクラウドホスト", - "xpack.securitySolution.navLinks.assets.cloud_defend.title": "クラウド", "xpack.securitySolution.navLinks.assets.fleet.agents.title": "エージェント", "xpack.securitySolution.navLinks.assets.fleet.dataStreams.title": "データストリーム", "xpack.securitySolution.navLinks.assets.fleet.description": "ElasticElasticエージェントの集中管理", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index bc26887d91071..dabf905b637a9 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -33274,7 +33274,6 @@ "xpack.securitySolution.appLinks.category.entityAnalytics": "实体分析", "xpack.securitySolution.appLinks.category.investigations": "调查", "xpack.securitySolution.appLinks.category.management": "管理", - "xpack.securitySolution.appLinks.cloudDefendPoliciesDescription": "通过细粒度、灵活的运行时策略保护 Kubernetes 中的容器工作负载,使其免于受到攻击和出现漂移。", "xpack.securitySolution.appLinks.cloudSecurityPostureBenchmarksDescription": "查看用于云安全态势管理的基准规则。", "xpack.securitySolution.appLinks.cloudSecurityPostureDashboardDescription": "所有 CSP 集成中的结果概述。", "xpack.securitySolution.appLinks.coverageOverviewDashboard": "MITRE ATT&CK 支持", @@ -37623,8 +37622,6 @@ "xpack.securitySolution.navigation.rules": "规则", "xpack.securitySolution.navigation.timelines": "时间线", "xpack.securitySolution.navigation.users": "用户", - "xpack.securitySolution.navLinks.assets.cloud_defend.description": "运行 Elastic Defend 的云主机", - "xpack.securitySolution.navLinks.assets.cloud_defend.title": "云", "xpack.securitySolution.navLinks.assets.fleet.agents.title": "代理", "xpack.securitySolution.navLinks.assets.fleet.dataStreams.title": "数据流", "xpack.securitySolution.navLinks.assets.fleet.description": "Elastic 代理的集中管理", diff --git a/x-pack/solutions/security/plugins/cloud_defend/public/common/navigation/constants.ts b/x-pack/solutions/security/plugins/cloud_defend/public/common/navigation/constants.ts index c4de6caa61b84..69ada0b5dad13 100644 --- a/x-pack/solutions/security/plugins/cloud_defend/public/common/navigation/constants.ts +++ b/x-pack/solutions/security/plugins/cloud_defend/public/common/navigation/constants.ts @@ -15,7 +15,7 @@ const NAV_ITEMS_NAMES = { }; /** The base path for all cloud defend pages. */ -export const CLOUD_DEFEND_BASE_PATH = '/cloud_defend'; +const CLOUD_DEFEND_BASE_PATH = '/cloud_defend'; export const cloudDefendPages: Record = { policies: { diff --git a/x-pack/solutions/security/plugins/cloud_defend/public/index.ts b/x-pack/solutions/security/plugins/cloud_defend/public/index.ts index 9dcf8bd5b2760..cca2e7edecb56 100755 --- a/x-pack/solutions/security/plugins/cloud_defend/public/index.ts +++ b/x-pack/solutions/security/plugins/cloud_defend/public/index.ts @@ -8,7 +8,6 @@ import { CloudDefendPlugin } from './plugin'; export type { CloudDefendSecuritySolutionContext } from './types'; export { getSecuritySolutionLink } from './common/navigation/security_solution_links'; -export { CLOUD_DEFEND_BASE_PATH } from './common/navigation/constants'; export type { CloudDefendPageId } from './common/navigation/types'; // This exports static code and TypeScript types, diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index b9923301a4cf6..7fb27693cc8af 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -115,7 +115,6 @@ export const THREAT_INTELLIGENCE_PATH = '/threat_intelligence' as const; export const INVESTIGATIONS_PATH = '/investigations' as const; export const MACHINE_LEARNING_PATH = '/ml' as const; export const ASSETS_PATH = '/assets' as const; -export const CLOUD_DEFEND_PATH = '/cloud_defend' as const; export const ENDPOINTS_PATH = `${MANAGEMENT_PATH}/endpoints` as const; export const POLICIES_PATH = `${MANAGEMENT_PATH}/policy` as const; export const TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/trusted_apps` as const; diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/assets_links.ts b/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/assets_links.ts index b6bb88d7dd321..82ef40fbebf9e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/assets_links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/assets_links.ts @@ -6,11 +6,11 @@ */ import { SecurityPageName, ExternalPageName } from '@kbn/security-solution-navigation'; -import { ASSETS_PATH, CLOUD_DEFEND_PATH } from '../../../../../common/constants'; +import { ASSETS_PATH } from '../../../../../common/constants'; import { SECURITY_FEATURE_ID } from '../../../../../common'; import type { LinkItem } from '../../../../common/links/types'; import type { SolutionNavLink } from '../../../../common/links'; -import { IconEcctlLazy, IconFleetLazy } from './lazy_icons'; +import { IconFleetLazy } from './lazy_icons'; import * as i18n from './assets_translations'; // appLinks configures the Security Solution pages links @@ -24,19 +24,6 @@ const assetsAppLink: LinkItem = { links: [], // endpoints and cloudDefend links are added in createAssetsLinkFromManage }; -// TODO: define this Cloud Defend app link in security_solution plugin -const assetsCloudDefendAppLink: LinkItem = { - id: SecurityPageName.cloudDefend, - title: i18n.CLOUD_DEFEND_TITLE, - description: i18n.CLOUD_DEFEND_DESCRIPTION, - path: CLOUD_DEFEND_PATH, - capabilities: [`${SECURITY_FEATURE_ID}.show`], - landingIcon: IconEcctlLazy, - isBeta: true, - hideTimeline: true, - links: [], -}; - export const createAssetsLinkFromManage = (manageLink: LinkItem): LinkItem => { const assetsSubLinks = []; @@ -54,8 +41,6 @@ export const createAssetsLinkFromManage = (manageLink: LinkItem): LinkItem => { assetsSubLinks.push({ ...endpointsLink, links: endpointsSubLinks }); } - assetsSubLinks.push(assetsCloudDefendAppLink); - return { ...assetsAppLink, links: assetsSubLinks, diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/assets_translations.ts b/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/assets_translations.ts index c6bd6072b2106..96d68eb2581e6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/assets_translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/assets_translations.ts @@ -11,19 +11,6 @@ export const ASSETS_TITLE = i18n.translate('xpack.securitySolution.navLinks.asse defaultMessage: 'Assets', }); -export const CLOUD_DEFEND_TITLE = i18n.translate( - 'xpack.securitySolution.navLinks.assets.cloud_defend.title', - { - defaultMessage: 'Cloud', - } -); -export const CLOUD_DEFEND_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.navLinks.assets.cloud_defend.description', - { - defaultMessage: 'Cloud hosts running Elastic Defend', - } -); - export const FLEET_TITLE = i18n.translate('xpack.securitySolution.navLinks.assets.fleet.title', { defaultMessage: 'Fleet', }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/icons/ecctl.tsx b/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/icons/ecctl.tsx deleted file mode 100644 index 994ca883ed2c4..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/icons/ecctl.tsx +++ /dev/null @@ -1,41 +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 type { SVGProps } from 'react'; -import React from 'react'; -export const IconEcctl: React.FC> = ({ ...props }) => ( - - - - - - -); - -// eslint-disable-next-line import/no-default-export -export default IconEcctl; diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/lazy_icons.tsx b/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/lazy_icons.tsx index c49c4695b1493..31b084d8c3523 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/lazy_icons.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/app/solution_navigation/links/sections/lazy_icons.tsx @@ -20,7 +20,6 @@ const withSuspenseIcon = (Component: React.ComponentType< export const IconLensLazy = withSuspenseIcon(React.lazy(() => import('./icons/lens'))); export const IconEndpointLazy = withSuspenseIcon(React.lazy(() => import('./icons/endpoint'))); export const IconFleetLazy = withSuspenseIcon(React.lazy(() => import('./icons/fleet'))); -export const IconEcctlLazy = withSuspenseIcon(React.lazy(() => import('./icons/ecctl'))); export const IconTimelineLazy = withSuspenseIcon(React.lazy(() => import('./icons/timeline'))); export const IconOsqueryLazy = withSuspenseIcon(React.lazy(() => import('./icons/osquery'))); diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_defend/index.ts b/x-pack/solutions/security/plugins/security_solution/public/cloud_defend/index.ts index 4ec2329d36bd5..48b89223de096 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_defend/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/cloud_defend/index.ts @@ -6,12 +6,11 @@ */ import type { SecuritySubPlugin } from '../app/types'; -import { routes } from './routes'; export class CloudDefend { public setup() {} public start(): SecuritySubPlugin { - return { routes }; + return { routes: [] }; } } diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_defend/links.ts b/x-pack/solutions/security/plugins/security_solution/public/cloud_defend/links.ts deleted file mode 100644 index cbc0a710a7ba1..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_defend/links.ts +++ /dev/null @@ -1,27 +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 { getSecuritySolutionLink } from '@kbn/cloud-defend-plugin/public'; -import { i18n } from '@kbn/i18n'; -import type { SecurityPageName } from '../../common/constants'; -import { SECURITY_FEATURE_ID } from '../../common/constants'; -import type { LinkItem } from '../common/links/types'; -import { IconCloudDefend } from '../common/icons/cloud_defend'; - -const commonLinkProperties: Partial = { - hideTimeline: true, - capabilities: [`${SECURITY_FEATURE_ID}.show`], -}; - -export const cloudDefendLink: LinkItem = { - ...getSecuritySolutionLink('policies'), - description: i18n.translate('xpack.securitySolution.appLinks.cloudDefendPoliciesDescription', { - defaultMessage: - 'Secure container workloads in Kubernetes from attacks and drift through granular and flexible runtime policies.', - }), - landingIcon: IconCloudDefend, - ...commonLinkProperties, -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/cloud_defend/routes.tsx b/x-pack/solutions/security/plugins/security_solution/public/cloud_defend/routes.tsx deleted file mode 100644 index 18bd7641addf3..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/cloud_defend/routes.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import type { - CloudDefendPageId, - CloudDefendSecuritySolutionContext, -} from '@kbn/cloud-defend-plugin/public'; -import { CLOUD_DEFEND_BASE_PATH } from '@kbn/cloud-defend-plugin/public'; -import type { SecurityPageName, SecuritySubPluginRoutes } from '../app/types'; -import { useKibana } from '../common/lib/kibana'; -import { SecuritySolutionPageWrapper } from '../common/components/page_wrapper'; -import { SpyRoute } from '../common/utils/route/spy_routes'; -import { FiltersGlobal } from '../common/components/filters_global'; -import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper'; - -// This exists only for the type signature cast -const CloudDefendSpyRoute = ({ pageName, ...rest }: { pageName?: CloudDefendPageId }) => ( - -); - -const cloudDefendSecuritySolutionContext: CloudDefendSecuritySolutionContext = { - getFiltersGlobalComponent: () => FiltersGlobal, - getSpyRouteComponent: () => CloudDefendSpyRoute, -}; - -const CloudDefend = () => { - const { cloudDefend } = useKibana().services; - const CloudDefendRouter = cloudDefend.getCloudDefendRouter(); - - return ( - - - - - - ); -}; - -CloudDefend.displayName = 'CloudDefend'; - -export const routes: SecuritySubPluginRoutes = [ - { - path: CLOUD_DEFEND_BASE_PATH, - component: CloudDefend, - }, -]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/icons/cloud_defend.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/icons/cloud_defend.tsx deleted file mode 100644 index 44cd0f39250b5..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/common/icons/cloud_defend.tsx +++ /dev/null @@ -1,50 +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 type { SVGProps } from 'react'; -import React from 'react'; -export const IconCloudDefend: React.FC> = ({ ...props }) => ( - - - - - - - - - - - - - -); diff --git a/x-pack/solutions/security/plugins/security_solution/public/management/links.ts b/x-pack/solutions/security/plugins/security_solution/public/management/links.ts index 5629bf83b3d41..60b173f0c626a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/management/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/management/links.ts @@ -42,7 +42,6 @@ import { import { licenseService } from '../common/hooks/use_license'; import type { LinkItem } from '../common/links/types'; import type { StartPlugins } from '../types'; -import { cloudDefendLink } from '../cloud_defend/links'; import { links as notesLink } from '../notes/links'; import { IconConsole } from '../common/icons/console'; import { IconShield } from '../common/icons/shield'; @@ -216,7 +215,6 @@ export const links: LinkItem = { skipUrlState: true, hideTimeline: true, }, - cloudDefendLink, notesLink, ], }; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/navigation/navigation.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/navigation/navigation.cy.ts index eba2dbe770e48..2ea08ba56a85a 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/navigation/navigation.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/navigation/navigation.cy.ts @@ -67,7 +67,6 @@ import { MACHINE_LEARNING_LANDING_URL, ASSETS_URL, FLEET_URL, - CLOUD_DEFEND_URL, HOSTS_URL, } from '../../../urls/navigation'; import { RULES_MANAGEMENT_URL } from '../../../urls/rules_management'; @@ -377,10 +376,6 @@ describe('Serverless side navigation links', { tags: '@serverless' }, () => { navigateFromHeaderTo(ServerlessHeaders.FLEET, true); cy.url().should('include', FLEET_URL); }); - it('navigates to the Cloud defend page', () => { - navigateFromHeaderTo(ServerlessHeaders.CLOUD_DEFEND, true); - cy.url().should('include', CLOUD_DEFEND_URL); - }); it('navigates to the Machine learning landing page', () => { navigateFromHeaderTo(ServerlessHeaders.MACHINE_LEARNING, true); cy.url().should('include', MACHINE_LEARNING_LANDING_URL); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts b/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts index 4ac4463ae4db1..bf3c04a90395f 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/serverless_security_header.ts @@ -65,7 +65,6 @@ export const HOSTS = '[data-test-subj="solutionSideNavPanelLink-hosts"]'; export const FLEET = '[data-test-subj="solutionSideNavPanelLink-fleet:"]'; export const ENDPOINTS = '[data-test-subj="solutionSideNavPanelLink-endpoints"]'; -export const CLOUD_DEFEND = '[data-test-subj="solutionSideNavPanelLink-cloud_defend"]'; export const POLICIES = '[data-test-subj="solutionSideNavPanelLink-policy"]'; @@ -122,8 +121,7 @@ export const openNavigationPanelFor = (pageName: string) => { break; } case FLEET: - case ENDPOINTS: - case CLOUD_DEFEND: { + case ENDPOINTS: { panel = ASSETS_PANEL_BTN; break; } diff --git a/x-pack/test/security_solution_cypress/cypress/urls/navigation.ts b/x-pack/test/security_solution_cypress/cypress/urls/navigation.ts index e6640336c8bd1..eb31d57212769 100644 --- a/x-pack/test/security_solution_cypress/cypress/urls/navigation.ts +++ b/x-pack/test/security_solution_cypress/cypress/urls/navigation.ts @@ -15,7 +15,6 @@ export const DASHBOARDS_URL = '/app/security/dashboards'; export const ASSETS_URL = '/app/security/assets'; export const ENDPOINTS_URL = '/app/security/administration/endpoints'; -export const CLOUD_DEFEND_URL = '/app/security/cloud_defend'; export const POLICIES_URL = '/app/security/administration/policy'; export const TRUSTED_APPS_URL = '/app/security/administration/trusted_apps'; export const EVENT_FILTERS_URL = '/app/security/administration/event_filters'; From 63394e6bfdedf8c8e76a457673d662d57afdc2a0 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 3 Mar 2025 16:08:25 +0100 Subject: [PATCH 6/9] Show chrome in redirect apps to improve navigation between apps (#210586) ## Summary possibly fixes https://github.com/elastic/kibana/issues/210058 - make it `chromless:false, visibleIn: []` so that in-app redirects via /r/ route feel smoother as chrome will stay visible - `` places loading/error in the middle of the screen, remove scroll - `abortController.signal` to cancel redirect in-case user has already navigated away while waiting for shortUrl resolve (edge case) - EuiDelayRender around "Redirecting..." spinner - Also do it for `forwardApp` ### Demo (download and slowdown to see the difference): **before in between apps navigation** (chrome remounts for a moment, adds junk) https://github.com/user-attachments/assets/36d98ce9-8051-4f91-8dd9-7b406d613a73 **after in between apps navigation** (chome stays, feels smoother) https://github.com/user-attachments/assets/4ad46954-aefe-4bde-ac4c-d598aac8b8fe **before initial load** (chrome mounts only with discover) https://github.com/user-attachments/assets/2618f25d-85fe-4474-8800-cbcb06db4b8e **after initial load** (chrome mounts earlier, feels like apps loads faster) https://github.com/user-attachments/assets/1adc0af3-6c3c-4334-9157-fba2f008d8d5 - As a bonus, because chrome is rendered now, the error state is friendlier, as it allows to navigate away: ![Screenshot 2025-02-11 at 15 24 29](https://github.com/user-attachments/assets/98fab62a-0ae0-4cc4-8464-c5123470ea81) ![Screenshot 2025-02-11 at 15 24 51](https://github.com/user-attachments/assets/02c455ff-9251-48f5-975c-3586c1fcbc0e) --------- Co-authored-by: Elastic Machine --- .../public/forward_app/forward_app.ts | 1 - .../url_service/redirect/components/page.tsx | 12 ++++++----- .../url_service/redirect/redirect_manager.ts | 20 +++++++++++++------ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/platform/plugins/private/url_forwarding/public/forward_app/forward_app.ts b/src/platform/plugins/private/url_forwarding/public/forward_app/forward_app.ts index 188c01dcce6ca..e2db1d96fe241 100644 --- a/src/platform/plugins/private/url_forwarding/public/forward_app/forward_app.ts +++ b/src/platform/plugins/private/url_forwarding/public/forward_app/forward_app.ts @@ -16,7 +16,6 @@ export const createLegacyUrlForwardApp = ( forwards: ForwardDefinition[] ): App => ({ id: 'kibana', - chromeless: true, title: 'Legacy URL migration', appRoute: '/app/kibana#/', visibleIn: [], diff --git a/src/platform/plugins/shared/share/public/url_service/redirect/components/page.tsx b/src/platform/plugins/shared/share/public/url_service/redirect/components/page.tsx index c5f2a93450092..943e9db87c2e8 100644 --- a/src/platform/plugins/shared/share/public/url_service/redirect/components/page.tsx +++ b/src/platform/plugins/shared/share/public/url_service/redirect/components/page.tsx @@ -10,7 +10,7 @@ import * as React from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { EuiPageTemplate } from '@elastic/eui'; +import { EuiPageTemplate, EuiDelayRender } from '@elastic/eui'; import type { CustomBrandingSetup } from '@kbn/core-custom-branding-browser'; import type { ChromeDocTitle, ThemeServiceSetup } from '@kbn/core/public'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; @@ -42,7 +42,7 @@ export const Page: React.FC = ({ if (error) { return ( - + @@ -51,9 +51,11 @@ export const Page: React.FC = ({ return ( - - - + + + + + ); }; diff --git a/src/platform/plugins/shared/share/public/url_service/redirect/redirect_manager.ts b/src/platform/plugins/shared/share/public/url_service/redirect/redirect_manager.ts index c4dd843deed00..96813a6372d3c 100644 --- a/src/platform/plugins/shared/share/public/url_service/redirect/redirect_manager.ts +++ b/src/platform/plugins/shared/share/public/url_service/redirect/redirect_manager.ts @@ -35,8 +35,11 @@ export class RedirectManager { application.register({ id: 'r', title: 'Redirect endpoint', - chromeless: true, + visibleIn: [], mount: async (params) => { + const abortController = new AbortController(); + this.onMount(params.history.location, abortController.signal); + const { render } = await import('./render'); const [start] = await core.getStartServices(); const { chrome, uiSettings, userProfile } = start; @@ -50,9 +53,8 @@ export class RedirectManager { homeHref: getHomeHref(http, uiSettings), }); - this.onMount(params.history.location); - return () => { + abortController.abort(); unmount(); }; }, @@ -92,11 +94,11 @@ export class RedirectManager { }); } - public onMount(location: Location) { + public onMount(location: Location, abortSignal?: AbortSignal) { const pathname = location.pathname; const isShortUrlRedirectBySlug = pathname.startsWith('/s/'); if (isShortUrlRedirectBySlug) { - this.navigateToShortUrlBySlug(pathname.substring('/s/'.length)); + this.navigateToShortUrlBySlug(pathname.substring('/s/'.length), abortSignal); return; } const urlLocationSearch = location.search; @@ -104,17 +106,23 @@ export class RedirectManager { this.navigate(options); } - private navigateToShortUrlBySlug(slug: string) { + private navigateToShortUrlBySlug(slug: string, abortSignal?: AbortSignal) { (async () => { const urlService = this.deps.url; const shortUrls = urlService.shortUrls.get(null); const shortUrl = await shortUrls.resolve(slug); + + if (abortSignal?.aborted) + return; /* it means that the user navigated away before the short url resolved */ + const locatorId = shortUrl.data.locator.id; const locator = urlService.locators.get(locatorId); if (!locator) throw new Error(`Locator "${locatorId}" not found.`); const locatorState = shortUrl.data.locator.state; await locator.navigate(locatorState, { replace: true }); })().catch((error) => { + if (abortSignal?.aborted) return; + this.error$.next(error); // eslint-disable-next-line no-console console.error(error); From 6ce22f4a336caa774acd360e3f66b74517d0bad0 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 3 Mar 2025 16:20:31 +0100 Subject: [PATCH 7/9] [ResponseOps][MW] Allow users to delete MWs (#211399) Resolve: https://github.com/elastic/kibana/issues/198559 Resolve: https://github.com/elastic/kibana/issues/205269 Here I used the existing DELETE /internal/alerting/rules/maintenance_window/{id} API to delete MWs from the UI. I added an action to the MW table so users can delete MWs. And show a delete confirmation modal when users delete a MW from the UI. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --- .../use_delete_maintenance_window.test.tsx | 69 +++++++++++++++++++ .../hooks/use_delete_maintenance_window.tsx | 39 +++++++++++ .../components/maintenance_windows_list.tsx | 23 ++++++- .../components/table_actions_popover.test.tsx | 36 ++++++++++ .../components/table_actions_popover.tsx | 53 +++++++++++--- .../pages/maintenance_windows/translations.ts | 21 ++++++ .../maintenance_windows_api/delete.test.ts | 34 +++++++++ .../maintenance_windows_api/delete.ts | 20 ++++++ .../maintenance_windows_table.ts | 35 ++++++++-- 9 files changed, 314 insertions(+), 16 deletions(-) create mode 100644 x-pack/platform/plugins/shared/alerting/public/hooks/use_delete_maintenance_window.test.tsx create mode 100644 x-pack/platform/plugins/shared/alerting/public/hooks/use_delete_maintenance_window.tsx create mode 100644 x-pack/platform/plugins/shared/alerting/public/services/maintenance_windows_api/delete.test.ts create mode 100644 x-pack/platform/plugins/shared/alerting/public/services/maintenance_windows_api/delete.ts diff --git a/x-pack/platform/plugins/shared/alerting/public/hooks/use_delete_maintenance_window.test.tsx b/x-pack/platform/plugins/shared/alerting/public/hooks/use_delete_maintenance_window.test.tsx new file mode 100644 index 0000000000000..353aca0f98e45 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/public/hooks/use_delete_maintenance_window.test.tsx @@ -0,0 +1,69 @@ +/* + * 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 { waitFor, renderHook } from '@testing-library/react'; + +import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; +import { useDeleteMaintenanceWindow } from './use_delete_maintenance_window'; + +const mockAddDanger = jest.fn(); +const mockAddSuccess = jest.fn(); + +jest.mock('../utils/kibana_react', () => { + const originalModule = jest.requireActual('../utils/kibana_react'); + return { + ...originalModule, + useKibana: () => { + const { services } = originalModule.useKibana(); + return { + services: { + ...services, + notifications: { toasts: { addSuccess: mockAddSuccess, addDanger: mockAddDanger } }, + }, + }; + }, + }; +}); +jest.mock('../services/maintenance_windows_api/delete', () => ({ + deleteMaintenanceWindow: jest.fn(), +})); + +const { deleteMaintenanceWindow } = jest.requireMock('../services/maintenance_windows_api/delete'); + +let appMockRenderer: AppMockRenderer; + +describe('useDeleteMaintenanceWindow', () => { + beforeEach(() => { + jest.clearAllMocks(); + + appMockRenderer = createAppMockRenderer(); + }); + + it('should call onSuccess if api succeeds', async () => { + const { result } = renderHook(() => useDeleteMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + result.current.mutate({ maintenanceWindowId: '123' }); + + await waitFor(() => expect(mockAddSuccess).toBeCalledWith('Deleted maintenance window')); + }); + + it('should call onError if api fails', async () => { + deleteMaintenanceWindow.mockRejectedValue(''); + + const { result } = renderHook(() => useDeleteMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + result.current.mutate({ maintenanceWindowId: '123' }); + + await waitFor(() => + expect(mockAddDanger).toBeCalledWith('Failed to delete maintenance window.') + ); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/public/hooks/use_delete_maintenance_window.tsx b/x-pack/platform/plugins/shared/alerting/public/hooks/use_delete_maintenance_window.tsx new file mode 100644 index 0000000000000..11502e6884592 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/public/hooks/use_delete_maintenance_window.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useMutation } from '@tanstack/react-query'; +import { useKibana } from '../utils/kibana_react'; +import { deleteMaintenanceWindow } from '../services/maintenance_windows_api/delete'; + +export const useDeleteMaintenanceWindow = () => { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const mutationFn = ({ maintenanceWindowId }: { maintenanceWindowId: string }) => { + return deleteMaintenanceWindow({ http, maintenanceWindowId }); + }; + + return useMutation(mutationFn, { + onSuccess: () => { + toasts.addSuccess( + i18n.translate('xpack.alerting.maintenanceWindowsDeleteSuccess', { + defaultMessage: 'Deleted maintenance window', + }) + ); + }, + onError: () => { + toasts.addDanger( + i18n.translate('xpack.alerting.maintenanceWindowsDeleteFailure', { + defaultMessage: 'Failed to delete maintenance window.', + }) + ); + }, + }); +}; diff --git a/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx b/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx index 363e00414a8d8..255d8cdeef5d3 100644 --- a/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx +++ b/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx @@ -32,6 +32,7 @@ import { TableActionsPopover, TableActionsPopoverProps } from './table_actions_p import { useFinishMaintenanceWindow } from '../../../hooks/use_finish_maintenance_window'; import { useArchiveMaintenanceWindow } from '../../../hooks/use_archive_maintenance_window'; import { useFinishAndArchiveMaintenanceWindow } from '../../../hooks/use_finish_and_archive_maintenance_window'; +import { useDeleteMaintenanceWindow } from '../../../hooks/use_delete_maintenance_window'; interface MaintenanceWindowsListProps { isLoading: boolean; @@ -143,9 +144,24 @@ export const MaintenanceWindowsList = React.memo( [finishAndArchiveMaintenanceWindow, refreshData] ); + const { mutate: deleteMaintenanceWindow, isLoading: isLoadingDelete } = + useDeleteMaintenanceWindow(); + + const onDelete = useCallback( + (id: string) => + deleteMaintenanceWindow({ maintenanceWindowId: id }, { onSuccess: () => refreshData() }), + [deleteMaintenanceWindow, refreshData] + ); + const isMutatingOrLoading = useMemo(() => { - return isLoadingFinish || isLoadingArchive || isLoadingFinishAndArchive || isLoading; - }, [isLoadingFinish, isLoadingArchive, isLoadingFinishAndArchive, isLoading]); + return ( + isLoadingFinish || + isLoadingArchive || + isLoadingFinishAndArchive || + isLoadingDelete || + isLoading + ); + }, [isLoadingFinish, isLoadingArchive, isLoadingFinishAndArchive, isLoadingDelete, isLoading]); const actions: Array> = useMemo( () => [ @@ -161,12 +177,13 @@ export const MaintenanceWindowsList = React.memo( onCancel={onCancel} onArchive={onArchive} onCancelAndArchive={onCancelAndArchive} + onDelete={onDelete} /> ); }, }, ], - [isMutatingOrLoading, onArchive, onCancel, onCancelAndArchive, onEdit] + [isMutatingOrLoading, onArchive, onCancel, onCancelAndArchive, onDelete, onEdit] ); const columns = useMemo( diff --git a/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/table_actions_popover.test.tsx b/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/table_actions_popover.test.tsx index 4fdc2dddda820..de45d47c7579f 100644 --- a/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/table_actions_popover.test.tsx +++ b/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/table_actions_popover.test.tsx @@ -48,6 +48,7 @@ describe('TableActionsPopover', () => { onCancel={() => {}} onArchive={() => {}} onCancelAndArchive={() => {}} + onDelete={() => {}} /> ); @@ -64,12 +65,14 @@ describe('TableActionsPopover', () => { onCancel={() => {}} onArchive={() => {}} onCancelAndArchive={() => {}} + onDelete={() => {}} /> ); fireEvent.click(result.getByTestId('table-actions-icon-button')); expect(result.getByTestId('table-actions-edit')).toBeInTheDocument(); expect(result.getByTestId('table-actions-cancel')).toBeInTheDocument(); expect(result.getByTestId('table-actions-cancel-and-archive')).toBeInTheDocument(); + expect(result.getByTestId('table-actions-delete')).toBeInTheDocument(); }); test('it shows the correct actions when a maintenance window is upcoming', () => { @@ -82,11 +85,13 @@ describe('TableActionsPopover', () => { onCancel={() => {}} onArchive={() => {}} onCancelAndArchive={() => {}} + onDelete={() => {}} /> ); fireEvent.click(result.getByTestId('table-actions-icon-button')); expect(result.getByTestId('table-actions-edit')).toBeInTheDocument(); expect(result.getByTestId('table-actions-archive')).toBeInTheDocument(); + expect(result.getByTestId('table-actions-delete')).toBeInTheDocument(); }); test('it shows the correct actions when a maintenance window is finished', () => { @@ -99,11 +104,13 @@ describe('TableActionsPopover', () => { onCancel={() => {}} onArchive={() => {}} onCancelAndArchive={() => {}} + onDelete={() => {}} /> ); fireEvent.click(result.getByTestId('table-actions-icon-button')); expect(result.getByTestId('table-actions-edit')).toBeInTheDocument(); expect(result.getByTestId('table-actions-archive')).toBeInTheDocument(); + expect(result.getByTestId('table-actions-delete')).toBeInTheDocument(); }); test('it shows the correct actions when a maintenance window is archived', () => { @@ -116,10 +123,12 @@ describe('TableActionsPopover', () => { onCancel={() => {}} onArchive={() => {}} onCancelAndArchive={() => {}} + onDelete={() => {}} /> ); fireEvent.click(result.getByTestId('table-actions-icon-button')); expect(result.getByTestId('table-actions-unarchive')).toBeInTheDocument(); + expect(result.getByTestId('table-actions-delete')).toBeInTheDocument(); }); test('it shows the success toast when maintenance window id is copied', async () => { @@ -138,6 +147,7 @@ describe('TableActionsPopover', () => { onCancel={() => {}} onArchive={() => {}} onCancelAndArchive={() => {}} + onDelete={() => {}} /> ); @@ -150,4 +160,30 @@ describe('TableActionsPopover', () => { Object.assign(navigator, global.window.navigator.clipboard); }); + + test('it calls onDelete function when maintenance window is deleted', async () => { + const onDelete = jest.fn(); + const user = userEvent.setup(); + const result = appMockRenderer.render( + {}} + onCancel={() => {}} + onArchive={() => {}} + onCancelAndArchive={() => {}} + onDelete={onDelete} + /> + ); + + await user.click(await result.findByTestId('table-actions-icon-button')); + expect(await result.findByTestId('table-actions-delete')).toBeInTheDocument(); + + await user.click(await result.findByTestId('table-actions-delete')); + const deleteModalConfirmButton = await result.findByTestId('confirmModalConfirmButton'); + expect(deleteModalConfirmButton).toBeInTheDocument(); + await user.click(deleteModalConfirmButton); + expect(onDelete).toHaveBeenCalledWith('123'); + }); }); diff --git a/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/table_actions_popover.tsx b/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/table_actions_popover.tsx index 76a3881d50478..c7419933818b6 100644 --- a/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/table_actions_popover.tsx +++ b/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/components/table_actions_popover.tsx @@ -27,12 +27,13 @@ export interface TableActionsPopoverProps { onCancel: (id: string) => void; onArchive: (id: string, archive: boolean) => void; onCancelAndArchive: (id: string) => void; + onDelete: (id: string) => void; } -type ModalType = 'cancel' | 'cancelAndArchive' | 'archive' | 'unarchive'; +type ModalType = 'cancel' | 'cancelAndArchive' | 'archive' | 'unarchive' | 'delete'; type ActionType = ModalType | 'edit' | 'copyId'; export const TableActionsPopover: React.FC = React.memo( - ({ id, status, isLoading, onEdit, onCancel, onArchive, onCancelAndArchive }) => { + ({ id, status, isLoading, onEdit, onCancel, onArchive, onCancelAndArchive, onDelete }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false); const [modalType, setModalType] = useState(); @@ -104,6 +105,18 @@ export const TableActionsPopover: React.FC = React.mem }, subtitle: i18n.UNARCHIVE_MODAL_SUBTITLE, }, + delete: { + props: { + title: i18n.DELETE_MODAL_TITLE, + onConfirm: () => { + closeModal(); + onDelete(id); + }, + cancelButtonText: i18n.CANCEL, + confirmButtonText: i18n.DELETE_MODAL_TITLE, + }, + subtitle: i18n.DELETE_MODAL_SUBTITLE, + }, }; let m; if (isModalVisible && modalType) { @@ -121,7 +134,16 @@ export const TableActionsPopover: React.FC = React.mem ); } return m; - }, [id, modalType, isModalVisible, closeModal, onArchive, onCancel, onCancelAndArchive]); + }, [ + id, + modalType, + isModalVisible, + closeModal, + onArchive, + onCancel, + onCancelAndArchive, + onDelete, + ]); const items = useMemo(() => { const menuItems = { @@ -170,7 +192,7 @@ export const TableActionsPopover: React.FC = React.mem { closePopover(); showModal('cancelAndArchive'); @@ -183,7 +205,7 @@ export const TableActionsPopover: React.FC = React.mem { closePopover(); showModal('archive'); @@ -205,12 +227,25 @@ export const TableActionsPopover: React.FC = React.mem {i18n.TABLE_ACTION_UNARCHIVE} ), + delete: ( + { + closePopover(); + showModal('delete'); + }} + > + {i18n.TABLE_ACTION_DELETE} + + ), }; const statusMenuItemsMap: Record = { - running: ['edit', 'copyId', 'cancel', 'cancelAndArchive'], - upcoming: ['edit', 'copyId', 'archive'], - finished: ['edit', 'copyId', 'archive'], - archived: ['copyId', 'unarchive'], + running: ['edit', 'copyId', 'cancel', 'cancelAndArchive', 'delete'], + upcoming: ['edit', 'copyId', 'archive', 'delete'], + finished: ['edit', 'copyId', 'archive', 'delete'], + archived: ['copyId', 'unarchive', 'delete'], }; return statusMenuItemsMap[status].map((type) => menuItems[type]); }, [status, closePopover, onEdit, id, toasts, showModal]); diff --git a/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/translations.ts b/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/translations.ts index 1ee3f4b3ae246..2a61019a292ec 100644 --- a/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/translations.ts +++ b/x-pack/platform/plugins/shared/alerting/public/pages/maintenance_windows/translations.ts @@ -626,6 +626,20 @@ export const CANCEL_AND_ARCHIVE_MODAL_SUBTITLE = i18n.translate( } ); +export const DELETE_MODAL_TITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.deleteModal.title', + { + defaultMessage: 'Delete maintenance window', + } +); + +export const DELETE_MODAL_SUBTITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.deleteModal.subtitle', + { + defaultMessage: "You won't be able to recover a deleted maintenance window.", + } +); + export const ARCHIVE = i18n.translate('xpack.alerting.maintenanceWindows.archive', { defaultMessage: 'Archive', }); @@ -660,6 +674,13 @@ export const TABLE_ACTION_UNARCHIVE = i18n.translate( } ); +export const TABLE_ACTION_DELETE = i18n.translate( + 'xpack.alerting.maintenanceWindows.table.delete', + { + defaultMessage: 'Delete', + } +); + export const UNARCHIVE_MODAL_TITLE = i18n.translate( 'xpack.alerting.maintenanceWindows.unarchiveModal.title', { diff --git a/x-pack/platform/plugins/shared/alerting/public/services/maintenance_windows_api/delete.test.ts b/x-pack/platform/plugins/shared/alerting/public/services/maintenance_windows_api/delete.test.ts new file mode 100644 index 0000000000000..5646bedb01193 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/public/services/maintenance_windows_api/delete.test.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. + */ + +/* + * 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 { httpServiceMock } from '@kbn/core/public/mocks'; +import { deleteMaintenanceWindow } from './delete'; + +const http = httpServiceMock.createStartContract(); + +beforeEach(() => jest.resetAllMocks()); + +describe('deleteMaintenanceWindow', () => { + test('should call delete maintenance window api', async () => { + await deleteMaintenanceWindow({ + http, + maintenanceWindowId: '123', + }); + expect(http.delete.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/maintenance_window/123", + ] + `); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting/public/services/maintenance_windows_api/delete.ts b/x-pack/platform/plugins/shared/alerting/public/services/maintenance_windows_api/delete.ts new file mode 100644 index 0000000000000..ee0197def2cb5 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting/public/services/maintenance_windows_api/delete.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { HttpSetup } from '@kbn/core/public'; +import { INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH } from '../../../common'; + +export const deleteMaintenanceWindow = async ({ + http, + maintenanceWindowId, +}: { + http: HttpSetup; + maintenanceWindowId: string; +}): Promise => { + await http.delete>( + `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/${encodeURIComponent(maintenanceWindowId)}` + ); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/maintenance_windows/maintenance_windows_table.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/maintenance_windows/maintenance_windows_table.ts index cce590d455c29..c6294747cea82 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/maintenance_windows/maintenance_windows_table.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/maintenance_windows/maintenance_windows_table.ts @@ -20,14 +20,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const objectRemover = new ObjectRemover(supertest); const browser = getService('browser'); - // FLAKY: https://github.com/elastic/kibana/issues/205269 - // Failing: See https://github.com/elastic/kibana/issues/205269 - describe.skip('Maintenance windows table', function () { + describe('Maintenance windows table', function () { beforeEach(async () => { await pageObjects.common.navigateToApp('maintenanceWindows'); }); - after(async () => { + afterEach(async () => { await objectRemover.removeAll(); }); @@ -291,5 +289,34 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const listedOnSecondPageMWs = await testSubjects.findAll('list-item'); expect(listedOnSecondPageMWs.length).to.be(2); }); + + it('should delete a maintenance window', async () => { + const name = generateUniqueKey(); + await createMaintenanceWindow({ + name, + getService, + }); + + await browser.refresh(); + await pageObjects.maintenanceWindows.searchMaintenanceWindows(name); + + const listBefore = await pageObjects.maintenanceWindows.getMaintenanceWindowsList(); + expect(listBefore.length).to.eql(1); + + await testSubjects.click('table-actions-popover'); + await testSubjects.click('table-actions-delete'); + + await testSubjects.click('confirmModalConfirmButton'); + + await retry.try(async () => { + const toastTitle = await toasts.getTitleAndDismiss(); + expect(toastTitle).to.eql('Deleted maintenance window'); + }); + + await pageObjects.maintenanceWindows.searchMaintenanceWindows(name); + + const listAfter = await pageObjects.maintenanceWindows.getMaintenanceWindowsList(); + expect(listAfter.length).to.eql(0); + }); }); }; From 54fb1554670f63f914a0d86b1c9b9714eaa5e849 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 4 Mar 2025 02:53:33 +1100 Subject: [PATCH 8/9] skip failing test suite (#211516) --- .../functional/apps/dataset_quality/dataset_quality_summary.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/dataset_quality/dataset_quality_summary.ts b/x-pack/test/functional/apps/dataset_quality/dataset_quality_summary.ts index 2c744279fcacb..6a7950d950c6e 100644 --- a/x-pack/test/functional/apps/dataset_quality/dataset_quality_summary.ts +++ b/x-pack/test/functional/apps/dataset_quality/dataset_quality_summary.ts @@ -47,7 +47,8 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid ]); }; - describe('Dataset quality summary', () => { + // Failing: See https://github.com/elastic/kibana/issues/211516 + describe.skip('Dataset quality summary', () => { afterEach(async () => { await synthtrace.clean(); }); From 3fc5022e13371c838ac3b555f0135aea10eeb831 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 4 Mar 2025 02:54:04 +1100 Subject: [PATCH 9/9] skip failing test suite (#211517) --- .../tests/data_streams/stats.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/dataset_quality_api_integration/tests/data_streams/stats.spec.ts b/x-pack/test/dataset_quality_api_integration/tests/data_streams/stats.spec.ts index c4f183e0dc8e5..74971182d40e6 100644 --- a/x-pack/test/dataset_quality_api_integration/tests/data_streams/stats.spec.ts +++ b/x-pack/test/dataset_quality_api_integration/tests/data_streams/stats.spec.ts @@ -60,7 +60,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { } registry.when('Api Key privileges check', { config: 'basic' }, () => { - describe('index privileges', () => { + // Failing: See https://github.com/elastic/kibana/issues/211517 + describe.skip('index privileges', () => { it('returns user authorization as false for noAccessUser', async () => { const resp = await callApiAs('noAccessUser');