diff --git a/.buildkite/pipelines/code_coverage/daily.yml b/.buildkite/pipelines/code_coverage/daily.yml index 20b6b505306af..0ffcd8f377071 100644 --- a/.buildkite/pipelines/code_coverage/daily.yml +++ b/.buildkite/pipelines/code_coverage/daily.yml @@ -25,5 +25,5 @@ steps: depends_on: - jest - jest-integration - timeout_in_minutes: 30 + timeout_in_minutes: 50 key: ingest diff --git a/.buildkite/scripts/common/activate_service_account.sh b/.buildkite/scripts/common/activate_service_account.sh index e5cd116a7bce1..83e30e37b8f07 100755 --- a/.buildkite/scripts/common/activate_service_account.sh +++ b/.buildkite/scripts/common/activate_service_account.sh @@ -4,18 +4,18 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/vault_fns.sh" -BUCKET_OR_EMAIL="${1:-}" +CALL_ARGUMENT="${1:-}" GCLOUD_EMAIL_POSTFIX="elastic-kibana-ci.iam.gserviceaccount.com" GCLOUD_SA_PROXY_EMAIL="kibana-ci-sa-proxy@$GCLOUD_EMAIL_POSTFIX" -if [[ -z "$BUCKET_OR_EMAIL" ]]; then +if [[ -z "$CALL_ARGUMENT" ]]; then echo "Usage: $0 " exit 1 -elif [[ "$BUCKET_OR_EMAIL" == "--unset-impersonation" ]]; then +elif [[ "$CALL_ARGUMENT" == "--unset-impersonation" ]]; then echo "Unsetting impersonation" gcloud config unset auth/impersonate_service_account exit 0 -elif [[ "$BUCKET_OR_EMAIL" == "--logout-gcloud" ]]; then +elif [[ "$CALL_ARGUMENT" == "--logout-gcloud" ]]; then echo "Logging out of gcloud" if [[ -x "$(command -v gcloud)" ]] && [[ "$(gcloud auth list 2>/dev/null | grep $GCLOUD_SA_PROXY_EMAIL)" != "" ]]; then gcloud auth revoke $GCLOUD_SA_PROXY_EMAIL --no-user-output-enabled @@ -48,12 +48,12 @@ fi # Check if the arg is a service account e-mail or a bucket name EMAIL="" -if [[ "$BUCKET_OR_EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then - EMAIL="$BUCKET_OR_EMAIL" -elif [[ "$BUCKET_OR_EMAIL" =~ ^gs://* ]]; then - BUCKET_NAME="${BUCKET_OR_EMAIL:5}" +if [[ "$CALL_ARGUMENT" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then + EMAIL="$CALL_ARGUMENT" +elif [[ "$CALL_ARGUMENT" =~ ^gs://* ]]; then + BUCKET_NAME="${CALL_ARGUMENT:5}" else - BUCKET_NAME="$BUCKET_OR_EMAIL" + BUCKET_NAME="$CALL_ARGUMENT" fi if [[ -z "$EMAIL" ]]; then diff --git a/.buildkite/scripts/steps/code_coverage/reporting/downloadPrevSha.sh b/.buildkite/scripts/steps/code_coverage/reporting/downloadPrevSha.sh index a77cfbef54d55..a0399977457a4 100755 --- a/.buildkite/scripts/steps/code_coverage/reporting/downloadPrevSha.sh +++ b/.buildkite/scripts/steps/code_coverage/reporting/downloadPrevSha.sh @@ -8,6 +8,7 @@ gsutil -m cp -r gs://elastic-bekitzur-kibana-coverage-live/previous_pointer/prev # TODO: Activate after the above is removed #.buildkite/scripts/common/activate_service_account.sh gs://elastic-kibana-coverage-live #gsutil -m cp -r gs://elastic-kibana-coverage-live/previous_pointer/previous.txt . || echo "### Previous Pointer NOT FOUND?" +#.buildkite/scripts/common/activate_service_account.sh --unset-impersonation if [ -e ./previous.txt ]; then mv previous.txt downloaded_previous.txt diff --git a/.buildkite/scripts/steps/code_coverage/reporting/uploadPrevSha.sh b/.buildkite/scripts/steps/code_coverage/reporting/uploadPrevSha.sh index 42ef5faa5cd3d..2164a4cd64251 100755 --- a/.buildkite/scripts/steps/code_coverage/reporting/uploadPrevSha.sh +++ b/.buildkite/scripts/steps/code_coverage/reporting/uploadPrevSha.sh @@ -14,3 +14,4 @@ gsutil cp previous.txt gs://elastic-bekitzur-kibana-coverage-live/previous_point .buildkite/scripts/common/activate_service_account.sh gs://elastic-kibana-coverage-live gsutil cp previous.txt gs://elastic-kibana-coverage-live/previous_pointer/ +.buildkite/scripts/common/activate_service_account.sh --unset-impersonation diff --git a/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh b/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh index 5bd0c07cc9b9b..701704a3a8b23 100755 --- a/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh +++ b/.buildkite/scripts/steps/code_coverage/reporting/uploadStaticSite.sh @@ -4,29 +4,40 @@ set -euo pipefail xs=("$@") -# TODO: Safe to remove this after 2024-03-01 (https://github.com/elastic/kibana/issues/175904) - also clean up usages +# TODO: Safe to remove this block after 2024-03-01 (https://github.com/elastic/kibana/issues/175904) - also clean up usages +echo "--- Uploading static site (legacy)" uploadPrefix_old="gs://elastic-bekitzur-kibana-coverage-live/" uploadPrefixWithTimeStamp_old="${uploadPrefix_old}${TIME_STAMP}/" +uploadBase_old() { + for x in 'src/dev/code_coverage/www/index.html' 'src/dev/code_coverage/www/404.html'; do + gsutil -m -q cp -r -a public-read -z js,css,html "${x}" "${uploadPrefix_old}" + done +} +uploadRest_old() { + for x in "${xs[@]}"; do + gsutil -m -q cp -r -a public-read -z js,css,html "target/kibana-coverage/${x}-combined" "${uploadPrefixWithTimeStamp_old}" + done +} +.buildkite/scripts/common/activate_service_account.sh --logout-gcloud +uploadBase_old +uploadRest_old +echo "--- Uploading static site" uploadPrefix="gs://elastic-kibana-coverage-live/" uploadPrefixWithTimeStamp="${uploadPrefix}${TIME_STAMP}/" uploadBase() { for x in 'src/dev/code_coverage/www/index.html' 'src/dev/code_coverage/www/404.html'; do gsutil -m -q cp -r -z js,css,html "${x}" "${uploadPrefix}" - gsutil -m -q cp -r -a public-read -z js,css,html "${x}" "${uploadPrefix_old}" done } - uploadRest() { for x in "${xs[@]}"; do gsutil -m -q cp -r -z js,css,html "target/kibana-coverage/${x}-combined" "${uploadPrefixWithTimeStamp}" - gsutil -m -q cp -r -a public-read -z js,css,html "target/kibana-coverage/${x}-combined" "${uploadPrefixWithTimeStamp_old}" done } -echo "--- Uploading static site" - .buildkite/scripts/common/activate_service_account.sh gs://elastic-kibana-coverage-live uploadBase uploadRest +.buildkite/scripts/common/activate_service_account.sh --unset-impersonation diff --git a/config/kibana.yml b/config/kibana.yml index eb6950af11e30..c816337f881d4 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -133,6 +133,10 @@ # - name: metrics.ops # level: debug +# Enables debug logging on the browser (dev console) +#logging.browser.root: +# level: debug + # =================== System: Other =================== # The path where Kibana stores persistent data not saved in Elasticsearch. Defaults to data #path.data: data diff --git a/packages/analytics/client/src/analytics_client/analytics_client.ts b/packages/analytics/client/src/analytics_client/analytics_client.ts index 9e0c559cbdc55..1029a87f2b935 100644 --- a/packages/analytics/client/src/analytics_client/analytics_client.ts +++ b/packages/analytics/client/src/analytics_client/analytics_client.ts @@ -133,12 +133,9 @@ export class AnalyticsClient implements IAnalyticsClient { properties: eventData as unknown as Record, }; - // debug-logging before checking the opt-in status to help during development - if (this.initContext.isDev) { - this.initContext.logger.debug(`Report event "${eventType}"`, { - ebt_event: event, - }); - } + this.initContext.logger.debug(`Report event "${eventType}"`, { + ebt_event: event, + }); const optInConfig = this.optInConfig$.value; diff --git a/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts b/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts index 01b46679c7452..ad1b889a07b22 100644 --- a/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts +++ b/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts @@ -10,6 +10,7 @@ import type { PluginName, DiscoveredPlugin } from '@kbn/core-base-common'; import type { ThemeVersion } from '@kbn/ui-shared-deps-npm'; import type { EnvironmentMode, PackageInfo } from '@kbn/config'; import type { CustomBranding } from '@kbn/core-custom-branding-common'; +import type { BrowserLoggingConfig } from '@kbn/core-logging-common-internal'; /** @internal */ export interface InjectedMetadataClusterInfo { @@ -45,6 +46,7 @@ export interface InjectedMetadata { publicBaseUrl?: string; assetsHrefBase: string; clusterInfo: InjectedMetadataClusterInfo; + logging: BrowserLoggingConfig; env: { mode: EnvironmentMode; packageInfo: PackageInfo; diff --git a/packages/core/injected-metadata/core-injected-metadata-common-internal/tsconfig.json b/packages/core/injected-metadata/core-injected-metadata-common-internal/tsconfig.json index 32b30c506c223..991449b03d2f7 100644 --- a/packages/core/injected-metadata/core-injected-metadata-common-internal/tsconfig.json +++ b/packages/core/injected-metadata/core-injected-metadata-common-internal/tsconfig.json @@ -15,7 +15,8 @@ "@kbn/config", "@kbn/ui-shared-deps-npm", "@kbn/core-base-common", - "@kbn/core-custom-branding-common" + "@kbn/core-custom-branding-common", + "@kbn/core-logging-common-internal" ], "exclude": [ "target/**/*", diff --git a/packages/core/logging/core-logging-browser-internal/src/logging_system.test.ts b/packages/core/logging/core-logging-browser-internal/src/logging_system.test.ts index 75071169eef5f..0058104083f2b 100644 --- a/packages/core/logging/core-logging-browser-internal/src/logging_system.test.ts +++ b/packages/core/logging/core-logging-browser-internal/src/logging_system.test.ts @@ -6,19 +6,29 @@ * Side Public License, v 1. */ +import type { BrowserLoggingConfig } from '@kbn/core-logging-common-internal'; import { unsafeConsole } from '@kbn/security-hardening'; import { BrowserLoggingSystem } from './logging_system'; -describe('', () => { +describe('BrowserLoggingSystem', () => { const timestamp = new Date(Date.UTC(2012, 1, 1, 14, 33, 22, 11)); let mockConsoleLog: jest.SpyInstance; let loggingSystem: BrowserLoggingSystem; + const createLoggingConfig = (parts: Partial = {}): BrowserLoggingConfig => { + return { + root: { + level: 'warn', + }, + ...parts, + }; + }; + beforeEach(() => { mockConsoleLog = jest.spyOn(unsafeConsole, 'log').mockReturnValue(undefined); jest.spyOn(global, 'Date').mockImplementation(() => timestamp); - loggingSystem = new BrowserLoggingSystem({ logLevel: 'warn' }); + loggingSystem = new BrowserLoggingSystem(createLoggingConfig()); }); afterEach(() => { @@ -74,5 +84,37 @@ describe('', () => { ] `); }); + + it('allows to override the root logger level', () => { + loggingSystem = new BrowserLoggingSystem(createLoggingConfig({ root: { level: 'debug' } })); + + const logger = loggingSystem.get('foo.bar'); + logger.trace('some trace message'); + logger.debug('some debug message'); + logger.info('some info message'); + logger.warn('some warn message'); + logger.error('some error message'); + logger.fatal('some fatal message'); + + expect(mockConsoleLog.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "[2012-01-31T13:33:22.011-05:00][DEBUG][foo.bar] some debug message", + ], + Array [ + "[2012-01-31T08:33:22.011-05:00][INFO ][foo.bar] some info message", + ], + Array [ + "[2012-01-31T03:33:22.011-05:00][WARN ][foo.bar] some warn message", + ], + Array [ + "[2012-01-30T22:33:22.011-05:00][ERROR][foo.bar] some error message", + ], + Array [ + "[2012-01-30T17:33:22.011-05:00][FATAL][foo.bar] some fatal message", + ], + ] + `); + }); }); }); diff --git a/packages/core/logging/core-logging-browser-internal/src/logging_system.ts b/packages/core/logging/core-logging-browser-internal/src/logging_system.ts index 50f7c449996ba..155146dce772c 100644 --- a/packages/core/logging/core-logging-browser-internal/src/logging_system.ts +++ b/packages/core/logging/core-logging-browser-internal/src/logging_system.ts @@ -6,17 +6,13 @@ * Side Public License, v 1. */ -import { LogLevel, Logger, LoggerFactory, LogLevelId, DisposableAppender } from '@kbn/logging'; -import { getLoggerContext } from '@kbn/core-logging-common-internal'; +import { LogLevel, Logger, LoggerFactory, DisposableAppender } from '@kbn/logging'; +import { getLoggerContext, BrowserLoggingConfig } from '@kbn/core-logging-common-internal'; import type { LoggerConfigType } from './types'; import { BaseLogger } from './logger'; import { PatternLayout } from './layouts'; import { ConsoleAppender } from './appenders'; -export interface BrowserLoggingConfig { - logLevel: LogLevelId; -} - const CONSOLE_APPENDER_ID = 'console'; /** @@ -54,7 +50,7 @@ export class BrowserLoggingSystem implements IBrowserLoggingSystem { private getLoggerConfigByContext(context: string): LoggerConfigType { return { - level: this.loggingConfig.logLevel, + level: this.loggingConfig.root.level, appenders: [CONSOLE_APPENDER_ID], name: context, }; diff --git a/packages/core/logging/core-logging-common-internal/index.ts b/packages/core/logging/core-logging-common-internal/index.ts index 24d5e93316789..6c9f1c510a512 100644 --- a/packages/core/logging/core-logging-common-internal/index.ts +++ b/packages/core/logging/core-logging-common-internal/index.ts @@ -22,3 +22,4 @@ export { ROOT_CONTEXT_NAME, DEFAULT_APPENDER_NAME, } from './src'; +export type { BrowserLoggingConfig, BrowserRootLoggerConfig } from './src/browser_config'; diff --git a/packages/core/logging/core-logging-common-internal/src/browser_config.ts b/packages/core/logging/core-logging-common-internal/src/browser_config.ts new file mode 100644 index 0000000000000..ccdc57b9369b3 --- /dev/null +++ b/packages/core/logging/core-logging-common-internal/src/browser_config.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { LogLevelId } from '@kbn/logging'; + +/** + * @internal + */ +export interface BrowserLoggingConfig { + root: BrowserRootLoggerConfig; +} + +/** + * @internal + */ +export interface BrowserRootLoggerConfig { + level: LogLevelId; +} diff --git a/packages/core/logging/core-logging-server-internal/index.ts b/packages/core/logging/core-logging-server-internal/index.ts index 306770d7a988b..0bd464ae21cdd 100644 --- a/packages/core/logging/core-logging-server-internal/index.ts +++ b/packages/core/logging/core-logging-server-internal/index.ts @@ -9,6 +9,7 @@ export { config } from './src/logging_config'; export type { LoggingConfigType, + LoggingConfigWithBrowserType, loggerContextConfigSchema, loggerSchema, } from './src/logging_config'; diff --git a/packages/core/logging/core-logging-server-internal/src/logging_config.test.ts b/packages/core/logging/core-logging-server-internal/src/logging_config.test.ts index 41acd072b295d..764cce1b34dd5 100644 --- a/packages/core/logging/core-logging-server-internal/src/logging_config.test.ts +++ b/packages/core/logging/core-logging-server-internal/src/logging_config.test.ts @@ -12,6 +12,11 @@ test('`schema` creates correct schema with defaults.', () => { expect(config.schema.validate({})).toMatchInlineSnapshot(` Object { "appenders": Map {}, + "browser": Object { + "root": Object { + "level": "info", + }, + }, "loggers": Array [], "root": Object { "appenders": Array [ diff --git a/packages/core/logging/core-logging-server-internal/src/logging_config.ts b/packages/core/logging/core-logging-server-internal/src/logging_config.ts index 00eb1450f0abe..191e859a0fe6f 100644 --- a/packages/core/logging/core-logging-server-internal/src/logging_config.ts +++ b/packages/core/logging/core-logging-server-internal/src/logging_config.ts @@ -47,6 +47,12 @@ export const loggerSchema = schema.object({ level: levelSchema, }); +const browserConfig = schema.object({ + root: schema.object({ + level: levelSchema, + }), +}); + export const config = { path: 'logging', schema: schema.object({ @@ -63,6 +69,7 @@ export const config = { }), level: levelSchema, }), + browser: browserConfig, }), }; @@ -71,6 +78,10 @@ export type LoggingConfigType = Pick, 'loggers' | ' appenders: Map; }; +/** @internal */ +export type LoggingConfigWithBrowserType = LoggingConfigType & + Pick, 'browser'>; + /** * Config schema for validating the inputs to the {@link LoggingServiceStart.configure} API. * See {@link LoggerContextConfigType}. diff --git a/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap b/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap index b6fedfd8644e4..7dd7b1739075c 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap +++ b/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap @@ -57,6 +57,7 @@ Object { }, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -122,6 +123,7 @@ Object { "user": Object {}, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -191,6 +193,7 @@ Object { }, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -256,6 +259,7 @@ Object { "user": Object {}, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -321,6 +325,7 @@ Object { "user": Object {}, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -390,6 +395,7 @@ Object { "user": Object {}, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -455,6 +461,7 @@ Object { "user": Object {}, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -520,6 +527,7 @@ Object { "user": Object {}, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -594,6 +602,7 @@ Object { }, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -659,6 +668,7 @@ Object { "user": Object {}, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -733,6 +743,7 @@ Object { }, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -803,6 +814,7 @@ Object { "user": Object {}, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -868,6 +880,7 @@ Object { "user": Object {}, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -942,6 +955,7 @@ Object { "user": Object {}, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -1012,6 +1026,7 @@ Object { "user": Object {}, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { @@ -1082,6 +1097,7 @@ Object { "user": Object {}, }, }, + "logging": Any, "publicBaseUrl": "http://myhost.com/mock-server-basepath", "serverBasePath": "/mock-server-basepath", "theme": Object { diff --git a/packages/core/rendering/core-rendering-server-internal/src/render_utils.ts b/packages/core/rendering/core-rendering-server-internal/src/render_utils.ts index 51f15a2ba034d..6408b986a7c87 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/render_utils.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/render_utils.ts @@ -6,9 +6,16 @@ * Side Public License, v 1. */ +import { firstValueFrom } from 'rxjs'; import UiSharedDepsNpm from '@kbn/ui-shared-deps-npm'; import * as UiSharedDepsSrc from '@kbn/ui-shared-deps-src'; +import type { IConfigService } from '@kbn/config'; +import type { BrowserLoggingConfig } from '@kbn/core-logging-common-internal'; import type { UiSettingsParams, UserProvidedValues } from '@kbn/core-ui-settings-common'; +import { + config as loggingConfigDef, + type LoggingConfigWithBrowserType, +} from '@kbn/core-logging-server-internal'; export const getSettingValue = ( settingName: string, @@ -54,3 +61,12 @@ export const getStylesheetPaths = ({ ]), ]; }; + +export const getBrowserLoggingConfig = async ( + configService: IConfigService +): Promise => { + const loggingConfig = await firstValueFrom( + configService.atPath(loggingConfigDef.path) + ); + return loggingConfig.browser; +}; diff --git a/packages/core/rendering/core-rendering-server-internal/src/rendering_service.test.mocks.ts b/packages/core/rendering/core-rendering-server-internal/src/rendering_service.test.mocks.ts index 580b9ca90dfa1..e4abd8b3ff95b 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/rendering_service.test.mocks.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/rendering_service.test.mocks.ts @@ -17,8 +17,10 @@ jest.doMock('./bootstrap', () => ({ export const getSettingValueMock = jest.fn(); export const getStylesheetPathsMock = jest.fn(); +export const getBrowserLoggingConfigMock = jest.fn(); jest.doMock('./render_utils', () => ({ getSettingValue: getSettingValueMock, getStylesheetPaths: getStylesheetPathsMock, + getBrowserLoggingConfig: getBrowserLoggingConfigMock, })); diff --git a/packages/core/rendering/core-rendering-server-internal/src/rendering_service.test.ts b/packages/core/rendering/core-rendering-server-internal/src/rendering_service.test.ts index 521e697f29a40..4cedc33b1b79d 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/rendering_service.test.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/rendering_service.test.ts @@ -11,6 +11,7 @@ import { bootstrapRendererMock, getSettingValueMock, getStylesheetPathsMock, + getBrowserLoggingConfigMock, } from './rendering_service.test.mocks'; import { load } from 'cheerio'; @@ -32,6 +33,7 @@ const INJECTED_METADATA = { version: expect.any(String), branch: expect.any(String), buildNumber: expect.any(Number), + logging: expect.any(Object), env: { mode: { name: expect.any(String), @@ -199,6 +201,22 @@ function renderTestCases( const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); expect(data).toMatchSnapshot(INJECTED_METADATA); }); + + it('renders "core" with logging config injected', async () => { + const loggingConfig = { + root: { + level: 'info', + }, + }; + getBrowserLoggingConfigMock.mockReturnValue(loggingConfig); + const [render] = await getRender(); + const content = await render(createKibanaRequest(), uiSettings, { + isAnonymousPage: false, + }); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); + expect(data.logging).toEqual(loggingConfig); + }); }); } @@ -418,8 +436,9 @@ describe('RenderingService', () => { jest.clearAllMocks(); service = new RenderingService(mockRenderingServiceParams); - getSettingValueMock.mockImplementation((settingName: string) => settingName); - getStylesheetPathsMock.mockReturnValue(['/style-1.css', '/style-2.css']); + getSettingValueMock.mockReset().mockImplementation((settingName: string) => settingName); + getStylesheetPathsMock.mockReset().mockReturnValue(['/style-1.css', '/style-2.css']); + getBrowserLoggingConfigMock.mockReset().mockReturnValue({}); }); describe('preboot()', () => { diff --git a/packages/core/rendering/core-rendering-server-internal/src/rendering_service.tsx b/packages/core/rendering/core-rendering-server-internal/src/rendering_service.tsx index f5bb1f5fa115f..3dde32bc72972 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/rendering_service.tsx +++ b/packages/core/rendering/core-rendering-server-internal/src/rendering_service.tsx @@ -29,7 +29,7 @@ import { RenderingMetadata, } from './types'; import { registerBootstrapRoute, bootstrapRendererFactory } from './bootstrap'; -import { getSettingValue, getStylesheetPaths } from './render_utils'; +import { getSettingValue, getStylesheetPaths, getBrowserLoggingConfig } from './render_utils'; import { filterUiPlugins } from './filter_ui_plugins'; import type { InternalRenderingRequestHandlerContext } from './internal_types'; @@ -185,6 +185,8 @@ export class RenderingService { buildNum, }); + const loggingConfig = await getBrowserLoggingConfig(this.coreContext.configService); + const filteredPlugins = filterUiPlugins({ uiPlugins, isAnonymousPage }); const bootstrapScript = isAnonymousPage ? 'bootstrap-anonymous.js' : 'bootstrap.js'; const metadata: RenderingMetadata = { @@ -210,6 +212,7 @@ export class RenderingService { serverBasePath, publicBaseUrl, assetsHrefBase: staticAssetsHrefBase, + logging: loggingConfig, env, clusterInfo, anonymousStatusPage: status?.isStatusPageAnonymous() ?? false, diff --git a/packages/core/rendering/core-rendering-server-internal/tsconfig.json b/packages/core/rendering/core-rendering-server-internal/tsconfig.json index c689fe370e784..ba9dfdd87f307 100644 --- a/packages/core/rendering/core-rendering-server-internal/tsconfig.json +++ b/packages/core/rendering/core-rendering-server-internal/tsconfig.json @@ -39,6 +39,8 @@ "@kbn/core-custom-branding-server-mocks", "@kbn/core-user-settings-server-mocks", "@kbn/core-user-settings-server-internal", + "@kbn/core-logging-common-internal", + "@kbn/core-logging-server-internal", ], "exclude": [ "target/**/*", diff --git a/packages/core/root/core-root-browser-internal/src/core_system.test.ts b/packages/core/root/core-root-browser-internal/src/core_system.test.ts index 5d9c1533a58a3..54dfeec3934dd 100644 --- a/packages/core/root/core-root-browser-internal/src/core_system.test.ts +++ b/packages/core/root/core-root-browser-internal/src/core_system.test.ts @@ -80,6 +80,11 @@ const defaultCoreSystemParams = { version: '1.2.3', }, }, + logging: { + root: { + level: 'debug', + }, + }, version: 'version', } as any, }; @@ -192,40 +197,27 @@ describe('constructor', () => { }); describe('logging system', () => { - it('instantiate the logging system with the correct level when in dev mode', () => { + it('instantiate the logging system with the correct level', () => { const envMode: EnvironmentMode = { name: 'development', dev: true, prod: false, }; - const injectedMetadata = { env: { mode: envMode } } as any; + const injectedMetadata = { + ...defaultCoreSystemParams.injectedMetadata, + env: { mode: envMode }, + } as any; createCoreSystem({ injectedMetadata, }); expect(LoggingSystemConstructor).toHaveBeenCalledTimes(1); - expect(LoggingSystemConstructor).toHaveBeenCalledWith({ - logLevel: 'all', - }); + expect(LoggingSystemConstructor).toHaveBeenCalledWith( + defaultCoreSystemParams.injectedMetadata.logging + ); }); - it('instantiate the logging system with the correct level when in production mode', () => { - const envMode: EnvironmentMode = { - name: 'production', - dev: false, - prod: true, - }; - const injectedMetadata = { env: { mode: envMode } } as any; - - createCoreSystem({ - injectedMetadata, - }); - expect(LoggingSystemConstructor).toHaveBeenCalledTimes(1); - expect(LoggingSystemConstructor).toHaveBeenCalledWith({ - logLevel: 'warn', - }); - }); it('retrieves the logger factory from the logging system', () => { createCoreSystem({}); expect(MockLoggingSystem.asLoggerFactory).toHaveBeenCalledTimes(1); diff --git a/packages/core/root/core-root-browser-internal/src/core_system.ts b/packages/core/root/core-root-browser-internal/src/core_system.ts index 406083969cba3..817bb9991cc05 100644 --- a/packages/core/root/core-root-browser-internal/src/core_system.ts +++ b/packages/core/root/core-root-browser-internal/src/core_system.ts @@ -7,7 +7,6 @@ */ import { filter, firstValueFrom } from 'rxjs'; -import type { LogLevelId } from '@kbn/logging'; import type { CoreContext } from '@kbn/core-base-browser-internal'; import { InjectedMetadataService, @@ -112,8 +111,7 @@ export class CoreSystem { this.rootDomElement = rootDomElement; - const logLevel: LogLevelId = injectedMetadata.env.mode.dev ? 'all' : 'warn'; - this.loggingSystem = new BrowserLoggingSystem({ logLevel }); + this.loggingSystem = new BrowserLoggingSystem(injectedMetadata.logging); this.injectedMetadata = new InjectedMetadataService({ injectedMetadata, diff --git a/packages/core/root/core-root-browser-internal/tsconfig.json b/packages/core/root/core-root-browser-internal/tsconfig.json index fc971f93f8fed..700ba5e74c5da 100644 --- a/packages/core/root/core-root-browser-internal/tsconfig.json +++ b/packages/core/root/core-root-browser-internal/tsconfig.json @@ -59,7 +59,6 @@ "@kbn/core-integrations-browser-mocks", "@kbn/core-apps-browser-mocks", "@kbn/core-logging-browser-mocks", - "@kbn/logging", "@kbn/config", "@kbn/core-custom-branding-browser-internal", "@kbn/core-custom-branding-browser-mocks", diff --git a/packages/kbn-text-based-editor/src/editor_footer.tsx b/packages/kbn-text-based-editor/src/editor_footer.tsx index 210988ac94e42..2d7196a3a774e 100644 --- a/packages/kbn-text-based-editor/src/editor_footer.tsx +++ b/packages/kbn-text-based-editor/src/editor_footer.tsx @@ -15,49 +15,18 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, - EuiPopover, - EuiPopoverTitle, - EuiDescriptionList, - EuiDescriptionListDescription, EuiButton, useEuiTheme, EuiLink, } from '@elastic/eui'; import { Interpolation, Theme, css } from '@emotion/react'; -import { css as classNameCss } from '@emotion/css'; - import type { MonacoMessage } from './helpers'; +import { ErrorsWarningsFooterPopover } from './errors_warnings_popover'; const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; const COMMAND_KEY = isMac ? '⌘' : '^'; const FEEDBACK_LINK = 'https://ela.st/esql-feedback'; -const getConstsByType = (type: 'error' | 'warning', count: number) => { - if (type === 'error') { - return { - color: 'danger', - message: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.errorCount', { - defaultMessage: '{count} {count, plural, one {error} other {errors}}', - values: { count }, - }), - label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.errorsTitle', { - defaultMessage: 'Errors', - }), - }; - } else { - return { - color: 'warning', - message: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.warningCount', { - defaultMessage: '{count} {count, plural, one {warning} other {warnings}}', - values: { count }, - }), - label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.warningsTitle', { - defaultMessage: 'Warnings', - }), - }; - } -}; - export function SubmitFeedbackComponent({ isSpaceReduced }: { isSpaceReduced?: boolean }) { const { euiTheme } = useEuiTheme(); return ( @@ -89,105 +58,6 @@ export function SubmitFeedbackComponent({ isSpaceReduced }: { isSpaceReduced?: b ); } -export function ErrorsWarningsPopover({ - isPopoverOpen, - items, - type, - setIsPopoverOpen, - onErrorClick, - isSpaceReduced, -}: { - isPopoverOpen: boolean; - items: MonacoMessage[]; - type: 'error' | 'warning'; - setIsPopoverOpen: (flag: boolean) => void; - onErrorClick: (error: MonacoMessage) => void; - isSpaceReduced?: boolean; -}) { - const strings = getConstsByType(type, items.length); - return ( - - - - { - setIsPopoverOpen(!isPopoverOpen); - }} - /> - - - { - setIsPopoverOpen(!isPopoverOpen); - }} - > -

{isSpaceReduced ? items.length : strings.message}

- - } - ownFocus={false} - isOpen={isPopoverOpen} - closePopover={() => setIsPopoverOpen(false)} - > -
- {strings.label} - - {items.map((item, index) => { - return ( - onErrorClick(item)} - > - - - - - - - - {i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.lineNumber', - { - defaultMessage: 'Line {lineNumber}', - values: { lineNumber: item.startLineNumber }, - } - )} - - - - - {item.message} - - - - ); - })} - -
-
-
-
-
- ); -} - interface EditorFooterProps { lines: number; containerCSS: Interpolation; @@ -271,7 +141,7 @@ export const EditorFooter = memo(function EditorFooter({ )} {errors && errors.length > 0 && ( - )} {warnings && warnings.length > 0 && ( - { + if (type === 'error') { + return { + color: 'danger', + message: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.errorCount', { + defaultMessage: '{count} {count, plural, one {error} other {errors}}', + values: { count }, + }), + label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.errorsTitle', { + defaultMessage: 'Errors', + }), + }; + } else { + return { + color: 'warning', + message: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.warningCount', { + defaultMessage: '{count} {count, plural, one {warning} other {warnings}}', + values: { count }, + }), + label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.warningsTitle', { + defaultMessage: 'Warnings', + }), + }; + } +}; + +export function ErrorsWarningsContent({ + items, + type, + onErrorClick, +}: { + items: MonacoMessage[]; + type: 'error' | 'warning'; + onErrorClick: (error: MonacoMessage) => void; +}) { + const { label, color } = getConstsByType(type, items.length); + return ( +
+ {label} + + {items.map((item, index) => { + return ( + onErrorClick(item)} + > + + + + + + + + {i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.lineNumber', { + defaultMessage: 'Line {lineNumber}', + values: { lineNumber: item.startLineNumber }, + })} + + + + + {item.message} + + + + ); + })} + +
+ ); +} + +export function ErrorsWarningsCompactViewPopover({ + items, + type, + onErrorClick, + popoverCSS, +}: { + items: MonacoMessage[]; + type: 'error' | 'warning'; + onErrorClick: (error: MonacoMessage) => void; + popoverCSS: Interpolation; +}) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const { color, message } = getConstsByType(type, items.length); + return ( + setIsPopoverOpen(true)} + onClickAriaLabel={message} + iconType={type} + iconSide="left" + data-test-subj={`TextBasedLangEditor-inline-${type}-badge`} + title={message} + css={css` + cursor: pointer; + `} + > + {items.length} + + } + css={popoverCSS} + ownFocus={false} + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + data-test-subj={`TextBasedLangEditor-inline-${type}-popover`} + > + + + ); +} + +export function ErrorsWarningsFooterPopover({ + isPopoverOpen, + items, + type, + setIsPopoverOpen, + onErrorClick, + isSpaceReduced, +}: { + isPopoverOpen: boolean; + items: MonacoMessage[]; + type: 'error' | 'warning'; + setIsPopoverOpen: (flag: boolean) => void; + onErrorClick: (error: MonacoMessage) => void; + isSpaceReduced?: boolean; +}) { + const { color, message } = getConstsByType(type, items.length); + return ( + + + + { + setIsPopoverOpen(!isPopoverOpen); + }} + /> + + + { + setIsPopoverOpen(!isPopoverOpen); + }} + > +

{isSpaceReduced ? items.length : message}

+ + } + ownFocus={false} + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + > + +
+
+
+
+ ); +} diff --git a/packages/kbn-text-based-editor/src/helpers.test.ts b/packages/kbn-text-based-editor/src/helpers.test.ts index 8ba691bb4e3ee..86a76e81c36de 100644 --- a/packages/kbn-text-based-editor/src/helpers.test.ts +++ b/packages/kbn-text-based-editor/src/helpers.test.ts @@ -90,7 +90,7 @@ describe('helpers', function () { ]); }); - it('should return the correct array of warnings if multiple warnins are detected', function () { + it('should return the correct array of warnings if multiple warnings are detected', function () { const warning = '299 Elasticsearch-8.10.0-SNAPSHOT-adb9fce96079b421c2575f0d2d445f492eb5f075 "Line 1:52: evaluation of [date_parse(geo.dest)] failed, treating result as null. Only first 20 failures recorded.", 299 Elasticsearch-8.10.0-SNAPSHOT-adb9fce96079b421c2575f0d2d445f492eb5f075 "Line 1:84: evaluation of [date_parse(geo.src)] failed, treating result as null. Only first 20 failures recorded."'; expect(parseWarning(warning)).toEqual([ @@ -115,6 +115,31 @@ describe('helpers', function () { ]); }); + it('should return the correct array of warnings if the message contains additional info', function () { + const warning = + '299 Elasticsearch-8.10.0-SNAPSHOT-adb9fce96079b421c2575f0d2d445f492eb5f075 "Line 1:52: evaluation of [date_parse(geo.dest)] failed, treating result as null. Only first 20 failures recorded.", 299 Elasticsearch-8.10.0-SNAPSHOT-adb9fce96079b421c2575f0d2d445f492eb5f075 "Line 1:84: java.lang.IllegalArgumentException: evaluation of [date_parse(geo.src)] failed, treating result as null. Only first 20 failures recorded."'; + expect(parseWarning(warning)).toEqual([ + { + endColumn: 138, + endLineNumber: 1, + message: + 'evaluation of [date_parse(geo.dest)] failed, treating result as null. Only first 20 failures recorded.', + severity: 4, + startColumn: 52, + startLineNumber: 1, + }, + { + endColumn: 169, + endLineNumber: 1, + message: + 'evaluation of [date_parse(geo.src)] failed, treating result as null. Only first 20 failures recorded.', + severity: 4, + startColumn: 84, + startLineNumber: 1, + }, + ]); + }); + it('should return the correct array of warnings if multiple warnins are detected without line indicators', function () { const warning = '299 Elasticsearch-8.10.0-SNAPSHOT-adb9fce96079b421c2575f0d2d445f492eb5f075 "Field [geo.coordinates] cannot be retrieved, it is unsupported or not indexed; returning null.", 299 Elasticsearch-8.10.0-SNAPSHOT-adb9fce96079b421c2575f0d2d445f492eb5f075 "Field [ip_range] cannot be retrieved, it is unsupported or not indexed; returning null.", 299 Elasticsearch-8.10.0-SNAPSHOT-adb9fce96079b421c2575f0d2d445f492eb5f075 "Field [timestamp_range] cannot be retrieved, it is unsupported or not indexed; returning null."'; diff --git a/packages/kbn-text-based-editor/src/helpers.ts b/packages/kbn-text-based-editor/src/helpers.ts index 80fd76bd4fd15..3d5a3290fc8e5 100644 --- a/packages/kbn-text-based-editor/src/helpers.ts +++ b/packages/kbn-text-based-editor/src/helpers.ts @@ -53,8 +53,10 @@ export const parseWarning = (warning: string): MonacoMessage[] => { // if there's line number encoded in the message use it as new positioning // and replace the actual message without it if (/Line (\d+):(\d+):/.test(warningMessage)) { - const [encodedLine, encodedColumn, innerMessage] = warningMessage.split(':'); - warningMessage = innerMessage; + const [encodedLine, encodedColumn, innerMessage, additionalInfoMessage] = + warningMessage.split(':'); + // sometimes the warning comes to the format java.lang.IllegalArgumentException: warning message + warningMessage = additionalInfoMessage ?? innerMessage; if (!Number.isNaN(Number(encodedColumn))) { startColumn = Number(encodedColumn); startLineNumber = Number(encodedLine.replace('Line ', '')); diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx index 0f9ed5c6cb9c5..114c8f6df510f 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.test.tsx @@ -114,25 +114,33 @@ describe('TextBasedLanguagesEditor', () => { ).toStrictEqual('@timestamp found'); }); - it('should render the errors badge for the inline mode by default if errors are provides', async () => { + it('should render the errors badge for the inline mode by default if errors are provided', async () => { const newProps = { ...props, errors: [new Error('error1')], }; const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps })); + const errorBadge = component.find('[data-test-subj="TextBasedLangEditor-inline-error-badge"]'); + expect(errorBadge.length).not.toBe(0); + errorBadge.at(0).simulate('click'); expect( - component.find('[data-test-subj="TextBasedLangEditor-inline-errors-badge"]').length + component.find('[data-test-subj="TextBasedLangEditor-inline-error-popover"]').length ).not.toBe(0); }); - it('should render the warnings badge for the inline mode by default if warning are provides', async () => { + it('should render the warnings badge for the inline mode by default if warning are provided', async () => { const newProps = { ...props, warning: 'Line 1: 20: Warning', }; const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps })); + const warningBadge = component.find( + '[data-test-subj="TextBasedLangEditor-inline-warning-badge"]' + ); + expect(warningBadge.length).not.toBe(0); + warningBadge.at(0).simulate('click'); expect( - component.find('[data-test-subj="TextBasedLangEditor-inline-warning-badge"]').length + component.find('[data-test-subj="TextBasedLangEditor-inline-warning-popover"]').length ).not.toBe(0); }); diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx index 241679734e248..87ec4142e9b36 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx @@ -62,6 +62,7 @@ import { import { EditorFooter } from './editor_footer'; import { ResizableButton } from './resizable_button'; import { fetchFieldsFromESQL } from './fetch_fields_from_esql'; +import { ErrorsWarningsCompactViewPopover } from './errors_warnings_popover'; import './overwrite.scss'; @@ -772,44 +773,22 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ )} {!isCompactFocused && editorMessages.errors.length > 0 && ( - - {editorMessages.errors.length} - + )} {!isCompactFocused && editorMessages.warnings.length > 0 && editorMessages.errors.length === 0 && ( - - {editorMessages.warnings.length} - + )} { @@ -82,3 +83,10 @@ export const getCaseUsersUrl = (id: string): string => { export const getCasesDeleteFileAttachmentsUrl = (id: string): string => { return INTERNAL_DELETE_FILE_ATTACHMENTS_URL.replace('{case_id}', id); }; + +export const getCustomFieldReplaceUrl = (caseId: string, customFieldId: string): string => { + return INTERNAL_PUT_CUSTOM_FIELDS_URL.replace('{case_id}', caseId).replace( + '{custom_field_id}', + customFieldId + ); +}; diff --git a/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx b/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx index 28be7c63f22b3..ca09d53501e5f 100644 --- a/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx @@ -14,9 +14,7 @@ import { screen, waitFor } from '@testing-library/react'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; import { SeverityFilter } from './severity_filter'; -// FLAKY: https://github.com/elastic/kibana/issues/176336 -// FLAKY: https://github.com/elastic/kibana/issues/176337 -describe.skip('Severity form field', () => { +describe('Severity form field', () => { const onChange = jest.fn(); let appMockRender: AppMockRenderer; const props = { @@ -30,28 +28,31 @@ describe.skip('Severity form field', () => { it('renders', async () => { appMockRender.render(); - expect(screen.getByTestId('options-filter-popover-button-severity')).toBeInTheDocument(); - expect(screen.getByTestId('options-filter-popover-button-severity')).not.toBeDisabled(); + expect(await screen.findByTestId('options-filter-popover-button-severity')).toBeInTheDocument(); + expect(await screen.findByTestId('options-filter-popover-button-severity')).not.toBeDisabled(); - userEvent.click(screen.getByRole('button', { name: 'Severity' })); + userEvent.click(await screen.findByRole('button', { name: 'Severity' })); await waitForEuiPopoverOpen(); - expect(screen.getByRole('option', { name: CaseSeverity.LOW })).toBeInTheDocument(); - expect(screen.getByRole('option', { name: CaseSeverity.MEDIUM })).toBeInTheDocument(); - expect(screen.getByRole('option', { name: CaseSeverity.HIGH })).toBeInTheDocument(); - expect(screen.getByRole('option', { name: CaseSeverity.CRITICAL })).toBeInTheDocument(); - expect(screen.getAllByRole('option').length).toBe(4); + expect(await screen.findByRole('option', { name: CaseSeverity.LOW })).toBeInTheDocument(); + expect(await screen.findByRole('option', { name: CaseSeverity.MEDIUM })).toBeInTheDocument(); + expect(await screen.findByRole('option', { name: CaseSeverity.HIGH })).toBeInTheDocument(); + expect(await screen.findByRole('option', { name: CaseSeverity.CRITICAL })).toBeInTheDocument(); + expect((await screen.findAllByRole('option')).length).toBe(4); }); it('selects the correct value when changed', async () => { appMockRender.render(); - userEvent.click(screen.getByRole('button', { name: 'Severity' })); + userEvent.click(await screen.findByRole('button', { name: 'Severity' })); await waitForEuiPopoverOpen(); - userEvent.click(screen.getByRole('option', { name: 'high' })); + userEvent.click(await screen.findByRole('option', { name: 'high' })); await waitFor(() => { - expect(onChange).toHaveBeenCalledWith({ filterId: 'severity', selectedOptionKeys: ['high'] }); + expect(onChange).toHaveBeenCalledWith({ + filterId: 'severity', + selectedOptionKeys: ['high'], + }); }); }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx index 74e04c4afac89..df6ea5de4d7c2 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx @@ -13,6 +13,8 @@ import { alertComment, basicCase, connectorsMock, + customFieldsConfigurationMock, + customFieldsMock, getCaseUsersMockResponse, getUserAction, } from '../../../containers/mock'; @@ -40,6 +42,7 @@ import { ConnectorTypes, UserActionTypes } from '../../../../common/types/domain import { CaseMetricsFeature } from '../../../../common/types/api'; import { useGetCaseConfiguration } from '../../../containers/configure/use_get_case_configuration'; import { useGetCurrentUserProfile } from '../../../containers/user_profiles/use_get_current_user_profile'; +import { useReplaceCustomField } from '../../../containers/use_replace_custom_field'; jest.mock('../../../containers/use_infinite_find_case_user_actions'); jest.mock('../../../containers/use_find_case_user_actions'); @@ -56,6 +59,7 @@ jest.mock('../../../containers/use_get_categories'); jest.mock('../../../containers/user_profiles/use_bulk_get_user_profiles'); jest.mock('../../../containers/use_get_case_connectors'); jest.mock('../../../containers/use_get_case_users'); +jest.mock('../../../containers/use_replace_custom_field'); jest.mock('../use_on_update_field'); jest.mock('../../../common/use_cases_features'); jest.mock('../../../containers/configure/use_get_case_configuration'); @@ -130,6 +134,8 @@ const useGetCasesFeaturesRes = { isSyncAlertsEnabled: true, }; +const replaceCustomField = jest.fn(); + const useFindCaseUserActionsMock = useFindCaseUserActions as jest.Mock; const useInfiniteFindCaseUserActionsMock = useInfiniteFindCaseUserActions as jest.Mock; const useGetCaseUserActionsStatsMock = useGetCaseUserActionsStats as jest.Mock; @@ -139,6 +145,7 @@ const useGetCaseConnectorsMock = useGetCaseConnectors as jest.Mock; const useGetCaseUsersMock = useGetCaseUsers as jest.Mock; const useOnUpdateFieldMock = useOnUpdateField as jest.Mock; const useCasesFeaturesMock = useCasesFeatures as jest.Mock; +const useReplaceCustomFieldMock = useReplaceCustomField as jest.Mock; describe('Case View Page activity tab', () => { let appMockRender: AppMockRenderer; @@ -169,6 +176,11 @@ describe('Case View Page activity tab', () => { isLoading: false, useOnUpdateField: jest.fn, }); + useReplaceCustomFieldMock.mockImplementation(() => ({ + isUpdatingCustomField: false, + isError: false, + mutate: replaceCustomField, + })); Object.defineProperty(window, 'getComputedStyle', { value: (el: HTMLElement) => { @@ -348,6 +360,36 @@ describe('Case View Page activity tab', () => { expect(await screen.findByTestId('case-view-edit-connector')).toBeInTheDocument(); }); + it('should call useReplaceCustomField correctly', async () => { + (useGetCaseConfiguration as jest.Mock).mockReturnValue({ + data: { + customFields: [customFieldsConfigurationMock[1]], + }, + }); + appMockRender.render( + + ); + + userEvent.click(await screen.findByRole('switch')); + + await waitFor(() => { + expect(replaceCustomField).toHaveBeenCalledWith({ + caseId: caseData.id, + caseVersion: caseData.version, + customFieldId: customFieldsMock[1].key, + customFieldValue: false, + }); + }); + + expect(await screen.findByTestId('case-view-edit-connector')).toBeInTheDocument(); + }); + describe('filter activity', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx index 1c579eaf08848..64d4c7f0cd327 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx @@ -40,6 +40,7 @@ import { Description } from '../../description'; import { EditCategory } from './edit_category'; import { parseCaseUsers } from '../../utils'; import { CustomFields } from './custom_fields'; +import { useReplaceCustomField } from '../../../containers/use_replace_custom_field'; export const CaseViewActivity = ({ ruleDetailsNavigation, @@ -93,6 +94,8 @@ export const CaseViewActivity = ({ caseData, }); + const { isLoading: isUpdatingCustomField, mutate: replaceCustomField } = useReplaceCustomField(); + const isLoadingAssigneeData = (isLoading && loadingKey === 'assignees') || isLoadingCaseUsers || isLoadingCurrentUserProfile; @@ -143,14 +146,16 @@ export const CaseViewActivity = ({ [onUpdateField] ); - const onSubmitCustomFields = useCallback( - (customFields: CaseUICustomField[]) => { - onUpdateField({ - key: 'customFields', - value: customFields, + const onSubmitCustomField = useCallback( + (customField: CaseUICustomField) => { + replaceCustomField({ + caseId: caseData.id, + customFieldId: customField.key, + customFieldValue: customField.value, + caseVersion: caseData.version, }); }, - [onUpdateField] + [replaceCustomField, caseData] ); const handleUserActionsActivityChanged = useCallback( @@ -290,10 +295,10 @@ export const CaseViewActivity = ({ /> ) : null} diff --git a/x-pack/plugins/cases/public/components/case_view/components/custom_fields.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/custom_fields.test.tsx index 9b1037959120b..a995e198774c9 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/custom_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/custom_fields.test.tsx @@ -121,37 +121,32 @@ describe('Case View Page files tab', () => { ).not.toBeInTheDocument(); }); - it('adds missing custom fields with no custom fields in the case', async () => { + it('removes extra custom fields', async () => { appMockRender.render( ); - userEvent.click((await screen.findAllByRole('switch'))[0]); + userEvent.click(await screen.findByRole('switch')); await waitFor(() => { - expect(onSubmit).toBeCalledWith([ - { - type: CustomFieldTypes.TEXT, - key: 'test_key_1', - value: customFieldsConfigurationMock[0].defaultValue, - }, - { type: CustomFieldTypes.TOGGLE, key: 'test_key_2', value: true }, - customFieldsMock[2], - customFieldsMock[3], - ]); + expect(onSubmit).toBeCalledWith({ + type: CustomFieldTypes.TOGGLE, + key: 'test_key_2', + value: false, + }); }); }); - it('adds missing custom fields with some custom fields in the case', async () => { + it('updates an existing toggle field correctly', async () => { appMockRender.render( @@ -160,130 +155,94 @@ describe('Case View Page files tab', () => { userEvent.click((await screen.findAllByRole('switch'))[0]); await waitFor(() => { - expect(onSubmit).toBeCalledWith([ - { - type: CustomFieldTypes.TEXT, - key: 'test_key_1', - value: customFieldsConfigurationMock[0].defaultValue, - }, - { type: CustomFieldTypes.TOGGLE, key: 'test_key_2', value: false }, - customFieldsMock[2], - customFieldsMock[3], - ]); + expect(onSubmit).toBeCalledWith({ + type: CustomFieldTypes.TOGGLE, + key: 'test_key_2', + value: false, + }); }); }); - it('adds missing defaultValues to required text custom fields without value', async () => { + it('updates new toggle field correctly', async () => { appMockRender.render( ); - // Clicking the toggle triggers the form submit userEvent.click((await screen.findAllByRole('switch'))[0]); await waitFor(() => { - expect(onSubmit).toBeCalledWith([ - { - type: CustomFieldTypes.TEXT, - key: 'test_key_1', - value: customFieldsConfigurationMock[0].defaultValue, - }, - { - type: CustomFieldTypes.TOGGLE, - key: 'test_key_2', - value: false, - }, - ]); + expect(onSubmit).toBeCalledWith({ + type: CustomFieldTypes.TOGGLE, + key: 'test_key_2', + value: true, + }); }); }); - it('does not overwrite existing text values with a configured defaultValue', async () => { + it('updates existing text field correctly', async () => { appMockRender.render( ); - userEvent.click((await screen.findAllByRole('switch'))[0]); - - await waitFor(() => { - expect(onSubmit).toBeCalledWith([ - { - type: CustomFieldTypes.TEXT, - key: 'test_key_1', - value: 'existing value', - }, - { - type: CustomFieldTypes.TOGGLE, - key: 'test_key_2', - value: false, - }, - ]); - }); - }); + userEvent.click( + await screen.findByTestId(`case-text-custom-field-edit-button-${customFieldsMock[0].key}`) + ); - it('removes extra custom fields', async () => { - appMockRender.render( - + userEvent.paste( + await screen.findByTestId('case-text-custom-field-form-field-test_key_1'), + '!!!' ); - userEvent.click(await screen.findByRole('switch')); + userEvent.click(await screen.findByTestId('case-text-custom-field-submit-button-test_key_1')); await waitFor(() => { - expect(onSubmit).toBeCalledWith([ - { type: CustomFieldTypes.TOGGLE, key: 'test_key_2', value: false }, - ]); + expect(onSubmit).toBeCalledWith({ + ...customFieldsMock[0], + value: 'My text test value 1!!!', + }); }); }); - it('updates an existing field correctly', async () => { + it('updates new text field correctly', async () => { appMockRender.render( ); - userEvent.click((await screen.findAllByRole('switch'))[0]); + userEvent.click( + await screen.findByTestId(`case-text-custom-field-edit-button-${customFieldsMock[0].key}`) + ); + + expect( + await screen.findByText('This field is populated with the default value.') + ).toBeInTheDocument(); + + userEvent.paste( + await screen.findByTestId('case-text-custom-field-form-field-test_key_1'), + ' updated!!' + ); + + userEvent.click(await screen.findByTestId('case-text-custom-field-submit-button-test_key_1')); await waitFor(() => { - expect(onSubmit).toBeCalledWith([ - customFieldsMock[0], - { type: CustomFieldTypes.TOGGLE, key: 'test_key_2', value: false }, - customFieldsMock[2], - customFieldsMock[3], - ]); + expect(onSubmit).toBeCalledWith({ + ...customFieldsMock[0], + value: `${customFieldsConfigurationMock[0].defaultValue} updated!!`, + }); }); }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/components/custom_fields.tsx b/x-pack/plugins/cases/public/components/case_view/components/custom_fields.tsx index b1bb01672c0dc..32d03bac8dc8e 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/custom_fields.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/custom_fields.tsx @@ -8,20 +8,16 @@ import React, { useCallback, useMemo } from 'react'; import { sortBy } from 'lodash'; import { EuiFlexItem } from '@elastic/eui'; -import type { - CasesConfigurationUI, - CasesConfigurationUICustomField, - CaseUICustomField, -} from '../../../../common/ui'; +import type { CasesConfigurationUI, CaseUICustomField } from '../../../../common/ui'; import type { CaseUI } from '../../../../common'; import { useCasesContext } from '../../cases_context/use_cases_context'; import { builderMap as customFieldsBuilderMap } from '../../custom_fields/builder'; -import { addOrReplaceCustomField } from '../../custom_fields/utils'; + interface Props { isLoading: boolean; customFields: CaseUI['customFields']; customFieldsConfiguration: CasesConfigurationUI['customFields']; - onSubmit: (customFields: CaseUICustomField[]) => void; + onSubmit: (customField: CaseUICustomField) => void; } const CustomFieldsComponent: React.FC = ({ @@ -38,16 +34,9 @@ const CustomFieldsComponent: React.FC = ({ const onSubmitCustomField = useCallback( (customFieldToAdd) => { - const allCustomFields = createMissingAndRemoveExtraCustomFields( - customFields, - customFieldsConfiguration - ); - - const updatedCustomFields = addOrReplaceCustomField(allCustomFields, customFieldToAdd); - - onSubmit(updatedCustomFields); + onSubmit(customFieldToAdd); }, - [customFields, customFieldsConfiguration, onSubmit] + [onSubmit] ); const customFieldsComponents = sortedCustomFieldsConfiguration.map((customFieldConf) => { @@ -87,36 +76,3 @@ const sortCustomFieldsByLabel = (customFieldsConfiguration: Props['customFieldsC return customFieldConf.label; }); }; - -const createMissingAndRemoveExtraCustomFields = ( - customFields: CaseUICustomField[], - confCustomFields: CasesConfigurationUICustomField[] -): CaseUICustomField[] => { - const createdCustomFields: CaseUICustomField[] = confCustomFields.map((confCustomField) => { - const foundCustomField = customFields.find( - (customField) => customField.key === confCustomField.key - ); - - const shouldUseDefaultValue = Boolean( - confCustomField.required && confCustomField?.defaultValue - ); - - if (foundCustomField) { - return { - ...foundCustomField, - value: - foundCustomField.value == null && shouldUseDefaultValue - ? confCustomField.defaultValue - : foundCustomField.value, - } as CaseUICustomField; - } - - return { - key: confCustomField.key, - type: confCustomField.type, - value: shouldUseDefaultValue ? confCustomField.defaultValue : null, - } as CaseUICustomField; - }); - - return createdCustomFields; -}; diff --git a/x-pack/plugins/cases/public/containers/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/__mocks__/api.ts index 9d99f658e9da0..a24c8b5b677c5 100644 --- a/x-pack/plugins/cases/public/containers/__mocks__/api.ts +++ b/x-pack/plugins/cases/public/containers/__mocks__/api.ts @@ -13,6 +13,7 @@ import type { CasesStatus, FetchCasesProps, FindCaseUserActions, + CaseUICustomField, } from '../types'; import { SortFieldCase } from '../types'; import { @@ -30,6 +31,7 @@ import { findCaseUserActionsResponse, getCaseUserActionsStatsResponse, getCaseUsersMockResponse, + customFieldsMock, } from '../mock'; import type { CaseConnectors, @@ -178,3 +180,15 @@ export const deleteFileAttachments = async ({ export const getCategories = async (signal: AbortSignal): Promise => Promise.resolve(categories); + +export const replaceCustomField = async ({ + caseId, + customFieldId, + customFieldValue, + caseVersion, +}: { + caseId: string; + customFieldId: string; + customFieldValue: string | boolean | null; + caseVersion: string; +}): Promise => Promise.resolve(customFieldsMock[0]); diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index e2e0897d75ada..02d639b790885 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -40,6 +40,7 @@ import { getCaseUserActionsStats, deleteFileAttachments, getCategories, + replaceCustomField, } from './api'; import { @@ -64,6 +65,7 @@ import { basicPushSnake, getCaseUserActionsStatsResponse, basicFileMock, + customFieldsMock, } from './mock'; import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './constants'; @@ -1152,4 +1154,40 @@ describe('Cases API', () => { expect(resp).toEqual({ 'servicenow-1': connectorCamelCase }); }); }); + + describe('replaceCustomField', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(customFieldsMock[0]); + }); + + const data = { + caseId: basicCase.id, + customFieldId: customFieldsMock[0].key, + request: { + value: 'this is an updated custom field', + caseVersion: basicCase.version, + }, + }; + + it('should be called with correct check url, method, signal', async () => { + await replaceCustomField({ ...data, signal: abortCtrl.signal }); + + expect(fetchMock).toHaveBeenCalledWith( + `${CASES_INTERNAL_URL}/${basicCase.id}/custom_fields/${customFieldsMock[0].key}`, + { + method: 'PUT', + body: JSON.stringify({ + ...data.request, + }), + signal: abortCtrl.signal, + } + ); + }); + + it('should return correct response', async () => { + const resp = await replaceCustomField({ ...data, signal: abortCtrl.signal }); + expect(resp).toEqual(customFieldsMock[0]); + }); + }); }); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index eeeffc6f4e424..020a4629552f4 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -7,7 +7,7 @@ import { ALERT_RULE_CONSUMER, ALERT_RULE_PRODUCER, ALERT_RULE_TYPE_ID } from '@kbn/rule-data-utils'; import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common/constants'; -import type { User } from '../../common/types/domain'; +import type { CaseCustomField, User } from '../../common/types/domain'; import { AttachmentType } from '../../common/types/domain'; import type { Case, Cases } from '../../common'; import type { @@ -21,6 +21,7 @@ import type { GetCaseConnectorsResponse, UserActionFindResponse, SingleCaseMetricsResponse, + CustomFieldPutRequest, } from '../../common/types/api'; import type { CaseConnectors, @@ -34,6 +35,7 @@ import type { CasesFindResponseUI, CasesUI, FilterOptions, + CaseUICustomField, } from '../../common/ui/types'; import { SortFieldCase } from '../../common/ui/types'; import { @@ -47,6 +49,7 @@ import { getCaseConnectorsUrl, getCaseUsersUrl, getCaseUserActionStatsUrl, + getCustomFieldReplaceUrl, } from '../../common/api'; import { CASE_REPORTERS_URL, @@ -367,6 +370,29 @@ export const updateCases = async ({ return convertCasesToCamelCase(decodeCasesResponse(response)); }; +export const replaceCustomField = async ({ + caseId, + customFieldId, + request, + signal, +}: { + caseId: string; + customFieldId: string; + request: CustomFieldPutRequest; + signal?: AbortSignal; +}): Promise => { + const response = await KibanaServices.get().http.fetch( + getCustomFieldReplaceUrl(caseId, customFieldId), + { + method: 'PUT', + body: JSON.stringify(request), + signal, + } + ); + + return convertToCamelCase(response); +}; + export const postComment = async ( newComment: AttachmentRequest, caseId: string, diff --git a/x-pack/plugins/cases/public/containers/constants.ts b/x-pack/plugins/cases/public/containers/constants.ts index 54e7cebba9025..0f57a729bc58b 100644 --- a/x-pack/plugins/cases/public/containers/constants.ts +++ b/x-pack/plugins/cases/public/containers/constants.ts @@ -63,6 +63,7 @@ export const casesMutationsKeys = { deleteFileAttachment: ['delete-file-attachment'] as const, bulkCreateAttachments: ['bulk-create-attachments'] as const, persistCaseConfiguration: ['persist-case-configuration'] as const, + replaceCustomField: ['replace-custom-field'] as const, }; const DEFAULT_SEARCH_FIELDS = ['title', 'description']; diff --git a/x-pack/plugins/cases/public/containers/use_replace_custom_field.test.tsx b/x-pack/plugins/cases/public/containers/use_replace_custom_field.test.tsx new file mode 100644 index 0000000000000..366d946af1d90 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_replace_custom_field.test.tsx @@ -0,0 +1,151 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { basicCase } from './mock'; +import * as api from './api'; +import type { AppMockRenderer } from '../common/mock'; +import { createAppMockRenderer } from '../common/mock'; +import { useToasts } from '../common/lib/kibana'; +import { casesQueriesKeys } from './constants'; +import { useReplaceCustomField } from './use_replace_custom_field'; + +jest.mock('./api'); +jest.mock('../common/lib/kibana'); + +describe('useReplaceCustomField', () => { + const sampleData = { + caseId: basicCase.id, + customFieldId: 'test_key_1', + customFieldValue: 'this is an updated custom field', + caseVersion: basicCase.version, + }; + + const addSuccess = jest.fn(); + const addError = jest.fn(); + (useToasts as jest.Mock).mockReturnValue({ addSuccess, addError }); + + let appMockRender: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRender = createAppMockRenderer(); + }); + + it('replace a customField and refresh the case page', async () => { + const queryClientSpy = jest.spyOn(appMockRender.queryClient, 'invalidateQueries'); + + const { waitForNextUpdate, result } = renderHook(() => useReplaceCustomField(), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => { + result.current.mutate(sampleData); + }); + + await waitForNextUpdate(); + + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.caseView()); + expect(queryClientSpy).toHaveBeenCalledWith(casesQueriesKeys.tags()); + }); + + it('calls the api when invoked with the correct parameters', async () => { + const patchCustomFieldSpy = jest.spyOn(api, 'replaceCustomField'); + const { waitForNextUpdate, result } = renderHook(() => useReplaceCustomField(), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => { + result.current.mutate(sampleData); + }); + + await waitForNextUpdate(); + + expect(patchCustomFieldSpy).toHaveBeenCalledWith({ + caseId: sampleData.caseId, + customFieldId: sampleData.customFieldId, + request: { + value: sampleData.customFieldValue, + caseVersion: sampleData.caseVersion, + }, + }); + }); + + it('calls the api when invoked with the correct parameters of toggle field', async () => { + const newData = { + caseId: basicCase.id, + customFieldId: 'test_key_2', + customFieldValue: false, + caseVersion: basicCase.version, + }; + const patchCustomFieldSpy = jest.spyOn(api, 'replaceCustomField'); + const { waitForNextUpdate, result } = renderHook(() => useReplaceCustomField(), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => { + result.current.mutate(newData); + }); + + await waitForNextUpdate(); + + expect(patchCustomFieldSpy).toHaveBeenCalledWith({ + caseId: newData.caseId, + customFieldId: newData.customFieldId, + request: { + value: newData.customFieldValue, + caseVersion: newData.caseVersion, + }, + }); + }); + + it('calls the api when invoked with the correct parameters with null value', async () => { + const newData = { + caseId: basicCase.id, + customFieldId: 'test_key_3', + customFieldValue: null, + caseVersion: basicCase.version, + }; + const patchCustomFieldSpy = jest.spyOn(api, 'replaceCustomField'); + const { waitForNextUpdate, result } = renderHook(() => useReplaceCustomField(), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => { + result.current.mutate(newData); + }); + + await waitForNextUpdate(); + + expect(patchCustomFieldSpy).toHaveBeenCalledWith({ + caseId: newData.caseId, + customFieldId: newData.customFieldId, + request: { + value: newData.customFieldValue, + caseVersion: newData.caseVersion, + }, + }); + }); + + it('shows a toast error when the api return an error', async () => { + jest + .spyOn(api, 'replaceCustomField') + .mockRejectedValue(new Error('useUpdateComment: Test error')); + + const { waitForNextUpdate, result } = renderHook(() => useReplaceCustomField(), { + wrapper: appMockRender.AppWrapper, + }); + + act(() => { + result.current.mutate(sampleData); + }); + + await waitForNextUpdate(); + + expect(addError).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/use_replace_custom_field.tsx b/x-pack/plugins/cases/public/containers/use_replace_custom_field.tsx new file mode 100644 index 0000000000000..5d2969f6e6d44 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_replace_custom_field.tsx @@ -0,0 +1,46 @@ +/* + * 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 { useMutation } from '@tanstack/react-query'; +import { useCasesToast } from '../common/use_cases_toast'; +import { useRefreshCaseViewPage } from '../components/case_view/use_on_refresh_case_view_page'; +import type { ServerError } from '../types'; +import { replaceCustomField } from './api'; +import { casesMutationsKeys } from './constants'; +import * as i18n from './translations'; + +interface ReplaceCustomField { + caseId: string; + customFieldId: string; + customFieldValue: string | boolean | null; + caseVersion: string; +} + +export const useReplaceCustomField = () => { + const { showErrorToast } = useCasesToast(); + const refreshCaseViewPage = useRefreshCaseViewPage(); + + return useMutation( + ({ caseId, customFieldId, customFieldValue, caseVersion }: ReplaceCustomField) => + replaceCustomField({ + caseId, + customFieldId, + request: { value: customFieldValue, caseVersion }, + }), + { + mutationKey: casesMutationsKeys.replaceCustomField, + onSuccess: () => { + refreshCaseViewPage(); + }, + onError: (error: ServerError) => { + showErrorToast(error, { title: i18n.ERROR_TITLE }); + }, + } + ); +}; + +export type UseReplaceCustomField = ReturnType; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling/full_content.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling/full_content.tsx index 90505a8356b35..ef4dfcb85fa02 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling/full_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling/full_content.tsx @@ -216,7 +216,11 @@ export const ConnectorContentScheduling: React.FC- + Whether to include prerelease packages in categories count (e.g. beta, rc, preview) + requestBody: + content: + application/json: + schema: + type: object + properties: + transforms: + type: array + items: + type: object + properties: + transformId: + type: string diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 857882c57525f..063261c307646 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -177,7 +177,7 @@ export const createMockAgentPolicyService = (): jest.Mocked { diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 26c1a3dcee268..b297bb6b128c2 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -11,6 +11,8 @@ import { securityMock } from '@kbn/security-plugin/server/mocks'; import { loggerMock } from '@kbn/logging-mocks'; import type { Logger } from '@kbn/core/server'; +import type { SavedObjectError } from '@kbn/core-saved-objects-common'; + import { PackagePolicyRestrictionRelatedError, FleetUnauthorizedError, @@ -1006,6 +1008,95 @@ describe('agent policy', () => { }); }); + describe('turnOffAgentTamperProtections', () => { + const createPolicySO = (id: string, isProtected: boolean, error?: SavedObjectError) => ({ + id, + type: AGENT_POLICY_SAVED_OBJECT_TYPE, + attributes: { + is_protected: isProtected, + }, + references: [], + score: 1, + ...(error ? { error } : {}), + }); + + const createMockSoClientThatReturns = (policies: Array>) => { + const mockSoClient = savedObjectsClientMock.create(); + + const resolvedValue = { + saved_objects: policies, + page: 1, + per_page: 10, + total: policies.length, + }; + mockSoClient.find.mockResolvedValue(resolvedValue); + return mockSoClient; + }; + + it('should return if all policies are compliant', async () => { + const mockSoClient = createMockSoClientThatReturns([]); + + expect(await agentPolicyService.turnOffAgentTamperProtections(mockSoClient)).toEqual({ + failedPolicies: [], + updatedPolicies: null, + }); + expect(mockSoClient.bulkUpdate).not.toHaveBeenCalled(); + }); + + it('should bulk update policies that are not compliant', async () => { + const mockSoClient = createMockSoClientThatReturns([ + createPolicySO('policy1', true), + createPolicySO('policy2', true), + createPolicySO('policy3', false), + ]); + + mockSoClient.bulkUpdate.mockResolvedValueOnce({ + saved_objects: [createPolicySO('policy1', false), createPolicySO('policy2', false)], + }); + + const expectedResponse = expect.arrayContaining([ + expect.objectContaining({ + id: 'policy1', + attributes: expect.objectContaining({ is_protected: false }), + }), + expect.objectContaining({ + id: 'policy2', + attributes: expect.objectContaining({ is_protected: false }), + }), + ]); + + expect(await agentPolicyService.turnOffAgentTamperProtections(mockSoClient)).toEqual({ + failedPolicies: [], + updatedPolicies: expectedResponse, + }); + + expect(mockSoClient.bulkUpdate).toHaveBeenCalledWith(expectedResponse); + }); + + it('should return failed policies if bulk update fails', async () => { + const mockSoClient = createMockSoClientThatReturns([ + createPolicySO('policy1', true), + createPolicySO('policy2', true), + createPolicySO('policy3', false), + ]); + mockSoClient.bulkUpdate.mockResolvedValueOnce({ + saved_objects: [ + createPolicySO('policy1', false, { error: 'Oops!', message: 'Ooops!', statusCode: 500 }), + createPolicySO('policy2', false), + ], + }); + expect(await agentPolicyService.turnOffAgentTamperProtections(mockSoClient)).toEqual({ + failedPolicies: [ + expect.objectContaining({ + id: 'policy1', + error: expect.objectContaining({ message: 'Ooops!' }), + }), + ], + updatedPolicies: [expect.objectContaining({ id: 'policy2' })], + }); + }); + }); + describe('deleteFleetServerPoliciesForPolicyId', () => { it('should call audit logger', async () => { const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 6795d81f28946..a532aab68b228 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { omit, isEqual, keyBy, groupBy, pick } from 'lodash'; +import { omit, isEqual, keyBy, groupBy, pick, chunk } from 'lodash'; import { v5 as uuidv5 } from 'uuid'; import { safeDump } from 'js-yaml'; import pMap from 'p-map'; @@ -22,6 +22,10 @@ import type { BulkResponseItem } from '@elastic/elasticsearch/lib/api/typesWithB import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; +import { asyncForEach } from '@kbn/std'; + +import type { SavedObjectError } from '@kbn/core-saved-objects-common'; + import { policyHasEndpointSecurity } from '../../common/services'; import { populateAssignedAgentsCount } from '../routes/agent_policy/handlers'; @@ -1303,6 +1307,70 @@ class AgentPolicyService { })); } + public async turnOffAgentTamperProtections(soClient: SavedObjectsClientContract): Promise<{ + updatedPolicies: Array> | null; + failedPolicies: Array<{ id: string; error: Error | SavedObjectError }>; + }> { + const { saved_objects: agentPoliciesWithEnabledAgentTamperProtection } = + await soClient.find({ + type: SAVED_OBJECT_TYPE, + page: 1, + perPage: SO_SEARCH_LIMIT, + filter: normalizeKuery(SAVED_OBJECT_TYPE, 'ingest-agent-policies.is_protected: true'), + fields: ['revision'], + }); + + if (agentPoliciesWithEnabledAgentTamperProtection.length === 0) { + return { + updatedPolicies: null, + failedPolicies: [], + }; + } + + const { saved_objects: updatedAgentPolicies } = + await soClient.bulkUpdate( + agentPoliciesWithEnabledAgentTamperProtection.map((agentPolicy) => { + const { id, attributes } = agentPolicy; + return { + id, + type: SAVED_OBJECT_TYPE, + attributes: { + is_protected: false, + revision: attributes.revision + 1, + updated_at: new Date().toISOString(), + updated_by: 'system', + }, + }; + }) + ); + + const failedPolicies: Array<{ + id: string; + error: Error | SavedObjectError; + }> = []; + + updatedAgentPolicies.forEach((policy) => { + if (policy.error) { + failedPolicies.push({ + id: policy.id, + error: policy.error, + }); + } + }); + + const updatedPoliciesSuccess = updatedAgentPolicies.filter((policy) => !policy.error); + + const config = appContextService.getConfig(); + const batchSize = config?.setup?.agentPolicySchemaUpgradeBatchSize ?? 100; + const policyIds = updatedPoliciesSuccess.map((policy) => policy.id); + await asyncForEach( + chunk(policyIds, batchSize), + async (policyIdsBatch) => await this.deployPolicies(soClient, policyIdsBatch) + ); + + return { updatedPolicies: updatedPoliciesSuccess, failedPolicies }; + } + private checkTamperProtectionLicense(agentPolicy: { is_protected?: boolean }): void { if (agentPolicy?.is_protected && !licenseService.isPlatinum()) { throw new FleetUnauthorizedError('Tamper protection requires Platinum license'); diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index ff219fd0d1ab6..6d7ca261e3bf3 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -19,7 +19,7 @@ export interface AgentPolicyServiceInterface { list: typeof agentPolicyService['list']; getFullAgentPolicy: typeof agentPolicyService['getFullAgentPolicy']; getByIds: typeof agentPolicyService['getByIDs']; - bumpRevision: typeof agentPolicyService['bumpRevision']; + turnOffAgentTamperProtections: typeof agentPolicyService['turnOffAgentTamperProtections']; } // Agent services diff --git a/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor_natural_language.tsx b/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor_natural_language.tsx index 6bc53cd5fbf03..6d752ce95e1f6 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor_natural_language.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor_natural_language.tsx @@ -40,11 +40,12 @@ export function PromptEditorNaturalLanguage({ const handleResizeTextArea = useCallback(() => { if (textAreaRef.current) { textAreaRef.current.style.minHeight = 'auto'; - textAreaRef.current.style.minHeight = textAreaRef.current?.scrollHeight + 'px'; - } - if (textAreaRef.current?.scrollHeight) { - onChangeHeight(textAreaRef.current.scrollHeight); + const cappedHeight = Math.min(textAreaRef.current?.scrollHeight, 350); + + textAreaRef.current.style.minHeight = cappedHeight + 'px'; + + onChangeHeight(cappedHeight); } }, [onChangeHeight]); @@ -54,12 +55,18 @@ export function PromptEditorNaturalLanguage({ if (textarea) { textarea.focus(); } - }, [handleResizeTextArea]); + }, []); useEffect(() => { handleResizeTextArea(); }, [handleResizeTextArea]); + useEffect(() => { + if (prompt === undefined) { + handleResizeTextArea(); + } + }, [handleResizeTextArea, prompt]); + return ( { + it('returns undefined for valid id', () => { + expect(idHookSchemaValidation('valid-id')).toBeUndefined(); + }); + it('returns undefined for valid id with numbers', () => { + expect(idHookSchemaValidation('123valid_id_123')).toBeUndefined(); + }); + it('returns undefined for valid id with underscore _', () => { + expect(idHookSchemaValidation('valid_id')).toBeUndefined(); + }); + it('returns error message for invalid id with spaces', () => { + expect(idHookSchemaValidation('invalid id')).toEqual( + 'Characters must be alphanumeric, _, or -' + ); + }); + + it('returns error message for invalid id with dots', () => { + expect(idHookSchemaValidation('invalid.id')).toEqual( + 'Characters must be alphanumeric, _, or -' + ); + }); + + it('returns error message for invalid id with special characters', () => { + expect(idHookSchemaValidation('invalid@id')).toEqual( + 'Characters must be alphanumeric, _, or -' + ); + }); + it('returns error message for invalid id just numbers', () => { + expect(idHookSchemaValidation('1232')).toEqual('Characters must be alphanumeric, _, or -'); + }); +}); diff --git a/x-pack/plugins/osquery/public/packs/queries/validations.ts b/x-pack/plugins/osquery/public/packs/queries/validations.ts index 37d74806fd1ae..f4f2f7831804b 100644 --- a/x-pack/plugins/osquery/public/packs/queries/validations.ts +++ b/x-pack/plugins/osquery/public/packs/queries/validations.ts @@ -9,7 +9,9 @@ import { i18n } from '@kbn/i18n'; import type { FormData, ValidationFunc } from '../../shared_imports'; export const MAX_QUERY_LENGTH = 2000; -const idPattern = /^[a-zA-Z0-9-_]+$/; + +// Has to be a string, can't be just numbers, and cannot contain dot '.' +const idPattern = /^(?![0-9]+$)[a-zA-Z0-9-_]+$/; // still used in Packs export const idSchemaValidation: ValidationFunc = ({ value }) => { const valueIsValid = idPattern.test(value); diff --git a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md index 19a15b64b5046..b60609b45be9d 100644 --- a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md +++ b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/installation_and_upgrade.md @@ -13,6 +13,7 @@ Status: `in progress`. The current test plan matches `Milestone 2` of the [Rule - [Non-functional requirements](#non-functional-requirements) - [Functional requirements](#functional-requirements) - [Scenarios](#scenarios) + - [Package installation](#package-installation) - [**Scenario: Package is installed via Fleet**](#scenario-package-is-installed-via-fleet) - [**Scenario: Package is installed via bundled Fleet package in Kibana**](#scenario-package-is-installed-via-bundled-fleet-package-in-kibana) @@ -63,6 +64,9 @@ Status: `in progress`. The current test plan matches `Milestone 2` of the [Rule - [**Scenario: Properties with semantically equal values should not be shown as modified**](#scenario-properties-with-semantically-equal-values-should-not-be-shown-as-modified) - [**Scenario: Unchanged sections of a rule should be hidden by default**](#scenario-unchanged-sections-of-a-rule-should-be-hidden-by-default) - [**Scenario: Properties should be sorted alphabetically**](#scenario-properties-should-be-sorted-alphabetically) + - [Rule upgrade workflow: preserving rule bound data](#rule-upgrade-workflow-preserving-rule-bound-data) + - [**Scenario: Rule bound data is preserved after upgrading a rule to a newer version with the same rule type**](#scenario-rule-bound-data-is-preserved-after-upgrading-a-rule-to-a-newer-version-with-the-same-rule-type) + - [**Scenario: Rule bound data is preserved after upgrading a rule to a newer version with a different rule type**](#scenario-rule-bound-data-is-preserved-after-upgrading-a-rule-to-a-newer-version-with-a-different-rule-type) - [Rule upgrade workflow: misc cases](#rule-upgrade-workflow-misc-cases) - [**Scenario: User doesn't see the Rule Updates tab until the package installation is completed**](#scenario-user-doesnt-see-the-rule-updates-tab-until-the-package-installation-is-completed) - [Error handling](#error-handling) @@ -949,6 +953,36 @@ When a user expands all hidden sections Then all properties of the rule should be sorted alphabetically ``` +### Rule upgrade workflow: preserving rule bound data + +#### **Scenario: Rule bound data is preserved after upgrading a rule to a newer version with the same rule type** + +**Automation**: 1 unit test per case, 1 integration test + +```Gherkin +Given a prebuilt rule is installed in Kibana +And this rule has an update available +And the update has the same rule type +When a user upgrades the rule +Then the rule bound data should be preserved +``` + +Examples: generated alerts, exception lists (rule exception list, shared exception list, endpoint exception list), timeline reference, actions, enabled state, execution results and execution events. + +#### **Scenario: Rule bound data is preserved after upgrading a rule to a newer version with a different rule type** + +**Automation**: 1 unit test per case, 1 integration test + +```Gherkin +Given a prebuilt rule is installed in Kibana +And this rule has an update available +And the update has a different rule type +When a user upgrades the rule +Then the rule bound data should be preserved +``` + +Examples: generated alerts, exception lists (rule exception list, shared exception list, endpoint exception list), timeline reference, actions, enabled state, execution results and execution events. + ### Rule upgrade workflow: misc cases #### **Scenario: User doesn't see the Rule Updates tab until the package installation is completed** diff --git a/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_step.test.tsx b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_step.test.tsx index 7ab5ca58ca0da..cf693392f83c4 100644 --- a/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_step.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_step.test.tsx @@ -10,9 +10,8 @@ import type { EuiTourStepProps } from '@elastic/eui'; import { GuidedOnboardingTourStep, SecurityTourStep } from './tour_step'; import { AlertsCasesTourSteps, SecurityStepId } from './tour_config'; import { useTourContext } from './tour'; -import { mockGlobalState, TestProviders } from '../../mock'; +import { mockGlobalState, TestProviders, createMockStore } from '../../mock'; import { TimelineId } from '../../../../common/types'; -import { createMockStore } from '../../mock/test_providers'; jest.mock('./tour'); const mockTourStep = jest diff --git a/x-pack/plugins/security_solution/public/common/mock/create_store.ts b/x-pack/plugins/security_solution/public/common/mock/create_store.ts new file mode 100644 index 0000000000000..91eac259e61f2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/mock/create_store.ts @@ -0,0 +1,30 @@ +/* + * 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 { Dispatch, Middleware } from 'redux'; +import { SUB_PLUGINS_REDUCER } from './utils'; +import type { State } from '../store'; +import { createStore } from '../store'; +import { mockGlobalState } from './global_state'; +import type { AppAction } from '../store/actions'; +import type { Immutable } from '../../../common/endpoint/types'; +import type { StartServices } from '../../types'; +import { createSecuritySolutionStorageMock } from './mock_local_storage'; + +const { storage: storageMock } = createSecuritySolutionStorageMock(); + +const kibanaMock = {} as unknown as StartServices; + +export const createMockStore = ( + state: State = mockGlobalState, + pluginsReducer: typeof SUB_PLUGINS_REDUCER = SUB_PLUGINS_REDUCER, + kibana: typeof kibanaMock = kibanaMock, + storage: typeof storageMock = storageMock, + additionalMiddleware?: Array>>> +) => { + return createStore(state, pluginsReducer, kibana, storage, additionalMiddleware); +}; diff --git a/x-pack/plugins/security_solution/public/common/mock/index.ts b/x-pack/plugins/security_solution/public/common/mock/index.ts index 89330cad8d478..d4cc1185846bb 100644 --- a/x-pack/plugins/security_solution/public/common/mock/index.ts +++ b/x-pack/plugins/security_solution/public/common/mock/index.ts @@ -18,3 +18,4 @@ export * from './netflow'; export * from './test_providers'; export * from './timeline_results'; export * from './utils'; +export * from './create_store'; diff --git a/x-pack/plugins/security_solution/public/common/mock/storybook_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/storybook_providers.tsx index 0fe2b8a2ecec9..6417180b8fd70 100644 --- a/x-pack/plugins/security_solution/public/common/mock/storybook_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/storybook_providers.tsx @@ -16,11 +16,8 @@ import { I18nProvider } from '@kbn/i18n-react'; import { CellActionsProvider } from '@kbn/cell-actions'; import { NavigationProvider } from '@kbn/security-solution-navigation'; import { CASES_FEATURE_ID } from '../../../common'; -import type { StartServices } from '../../types'; import { ReactQueryClientProvider } from '../containers/query_client/query_client_provider'; -import { createMockStore } from './test_providers'; - -export const kibanaMock = {} as unknown as StartServices; +import { createMockStore } from './create_store'; const uiSettings = { get: (setting: string) => { diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 17e0efa86773c..3d97f62767837 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -12,7 +12,7 @@ import React from 'react'; import type { DropResult, ResponderProvided } from '@hello-pangea/dnd'; import { DragDropContext } from '@hello-pangea/dnd'; import { Provider as ReduxStoreProvider } from 'react-redux'; -import type { Dispatch, Middleware, Store } from 'redux'; +import type { Store } from 'redux'; import { ThemeProvider } from 'styled-components'; import type { Capabilities } from '@kbn/core/public'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -24,22 +24,16 @@ import { useKibana } from '../lib/kibana'; import { UpsellingProvider } from '../components/upselling_provider'; import { MockAssistantProvider } from './mock_assistant_provider'; import { ConsoleManager } from '../../management/components/console'; -import type { State } from '../store'; -import { createStore } from '../store'; -import { mockGlobalState } from './global_state'; import { createKibanaContextProviderMock, createStartServicesMock, } from '../lib/kibana/kibana_react.mock'; import type { FieldHook } from '../../shared_imports'; -import { SUB_PLUGINS_REDUCER } from './utils'; -import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; +import { localStorageMock } from './mock_local_storage'; import { ASSISTANT_FEATURE_ID, CASES_FEATURE_ID } from '../../../common/constants'; import { UserPrivilegesProvider } from '../components/user_privileges/user_privileges_context'; import { MockDiscoverInTimelineContext } from '../components/discover_in_timeline/mocks/discover_in_timeline_provider'; -import type { AppAction } from '../store/actions'; -import type { Immutable } from '../../../common/endpoint/types'; - +import { createMockStore } from './create_store'; interface Props { children?: React.ReactNode; store?: Store; @@ -54,17 +48,6 @@ Object.defineProperty(window, 'localStorage', { }); window.scrollTo = jest.fn(); const MockKibanaContextProvider = createKibanaContextProviderMock(); -const { storage: storageMock } = createSecuritySolutionStorageMock(); - -export const createMockStore = ( - state: State = mockGlobalState, - pluginsReducer: typeof SUB_PLUGINS_REDUCER = SUB_PLUGINS_REDUCER, - kibana: typeof kibanaMock = kibanaMock, - storage: typeof storageMock = storageMock, - additionalMiddleware?: Array>>> -) => { - return createStore(state, pluginsReducer, kibana, storage, additionalMiddleware); -}; /** A utility for wrapping children in the providers required to run most tests */ export const TestProvidersComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/constants.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/constants.ts index 19d6111b5a936..7947b2647a27a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/constants.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/constants.ts @@ -12,7 +12,7 @@ export const getActionDetails = (actionTypeId: string) => { case ResponseActionTypesEnum['.osquery']: return { logo: 'logoOsquery', name: 'Osquery' }; case ResponseActionTypesEnum['.endpoint']: - return { logo: 'logoSecurity', name: 'Endpoint Security' }; + return { logo: 'logoSecurity', name: 'Elastic Defend' }; // update when new responseActions are provided default: return { logo: 'logoOsquery', name: 'Osquery' }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/callout.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/callout.test.tsx new file mode 100644 index 0000000000000..8387bf6f741e5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/callout.test.tsx @@ -0,0 +1,77 @@ +/* + * 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 { EndpointActionCallout } from './callout'; +import { render } from '@testing-library/react'; +import { useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +jest.mock('@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'); + +const useFormDataMock = useFormData as jest.MockedFunction; + +const mockFormData = (data: Record) => { + (useFormDataMock as jest.MockedFunction).mockReturnValue([ + data, + jest.fn(), + false, + ]); +}; + +describe('EndpointActionCallout', () => { + describe('isolate', () => { + beforeAll(() => { + mockFormData({ + 'test.command': 'isolate', + }); + }); + it('renders insufficient privileges warning when editDisabled', () => { + const { getByText } = render(); + expect(getByText('Insufficient privileges')).toBeInTheDocument(); + }); + + it('renders isolation caution for isolate command', () => { + const { queryByText, getByText } = render( + + ); + expect(getByText('Proceed with caution')).toBeInTheDocument(); + expect( + getByText( + 'Only select this option if you’re certain that you want to automatically block communication with other hosts on your network until you release this host.' + ) + ).toBeInTheDocument(); + expect( + queryByText( + 'Only select this option if you’re certain that you want to automatically terminate the process running on a host.' + ) + ).not.toBeInTheDocument(); + }); + }); + + describe('kill-process/suspend-process', () => { + beforeAll(() => { + mockFormData({ + 'test.command': 'kill-process', + }); + }); + it('renders process caution for kill-process/suspend-process commands', () => { + const { queryByText, getByText } = render( + + ); + expect(getByText('Proceed with caution')).toBeInTheDocument(); + expect( + getByText( + 'Only select this option if you’re certain that you want to automatically terminate the process running on a host.' + ) + ).toBeInTheDocument(); + expect( + queryByText( + 'Only select this option if you’re certain that you want to automatically block communication with other hosts on your network until you release this host.' + ) + ).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/callout.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/callout.tsx index 3468743f5d9d7..7516cf0fa5aaf 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/callout.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/callout.tsx @@ -10,6 +10,7 @@ import { EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { get } from 'lodash'; +import { CONSOLE_COMMANDS } from '../../../management/common/translations'; interface EndpointCallOutProps { basePath: string; @@ -34,12 +35,7 @@ const EndpointActionCalloutComponent = ({ basePath, editDisabled }: EndpointCall /> } > - - - + {CONSOLE_COMMANDS.isolate.privileges} @@ -88,7 +84,7 @@ const EndpointActionCalloutComponent = ({ basePath, editDisabled }: EndpointCall diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/check_permissions.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/check_permissions.test.ts new file mode 100644 index 0000000000000..9f4bf6724acf5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/check_permissions.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EndpointPrivileges } from '../../../../common/endpoint/types'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; +import { useCheckEndpointPermissions } from './check_permissions'; +jest.mock('../../../common/components/user_privileges'); + +const usePrivilegesMock = useUserPrivileges as jest.MockedFunction; + +const mockPrivileges = (config: Partial) => { + usePrivilegesMock.mockReturnValue({ + // @ts-expect-error missing some values that are not required for testing purposes + endpointPrivileges: config, + }); +}; + +describe('useCheckEndpointPermissions', () => { + const action = { + actionTypeId: '.endpoint' as const, + params: { + command: 'isolate' as const, + comment: 'test', + }, + }; + describe('with privileges', () => { + beforeAll(() => { + mockPrivileges({ + loading: false, + canIsolateHost: true, + canUnIsolateHost: true, + canKillProcess: true, + canSuspendProcess: true, + }); + }); + it('returns false when user has privileges for isolate', () => { + const result = useCheckEndpointPermissions(action); + + expect(result).toBe(false); + }); + it('returns false when user has privileges for kill-process', () => { + const result = useCheckEndpointPermissions({ + ...action, + params: { command: 'kill-process', config: { overwrite: true, field: '' } }, + }); + + expect(result).toBe(false); + }); + + it('returns undefined when actionTypeId is not a registered action', () => { + const result = useCheckEndpointPermissions({ + ...action, + // @ts-expect-error wrong value just for testing purposes + actionTypeId: 'notEndpoint', + }); + + expect(result).toBe(undefined); + }); + }); + + describe('without privileges', () => { + beforeEach(() => { + mockPrivileges({ + loading: false, + canIsolateHost: false, + canUnIsolateHost: false, + canKillProcess: false, + canSuspendProcess: false, + }); + }); + + it('return true if user has no privilege to execute a command', () => { + const result = useCheckEndpointPermissions(action); + + expect(result).toBe(true); + }); + + it('returns false when action is not endpoint command', () => { + const result = useCheckEndpointPermissions({ + ...action, + // @ts-expect-error wrong value just for testing purposes + actionTypeId: 'notEndpoint', + }); + expect(result).toBe(undefined); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/check_permissions.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/check_permissions.ts index 2fe8fb81fa169..26fdc8935ca3c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/check_permissions.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/check_permissions.ts @@ -10,6 +10,7 @@ import type { RuleResponseAction } from '../../../../common/api/detection_engine import { getRbacControl } from '../../../../common/endpoint/service/response_actions/utils'; import { useUserPrivileges } from '../../../common/components/user_privileges'; +// returns false if the user does have the required privileges to execute the action, returns true if the user does not have the required privileges export const useCheckEndpointPermissions = (action: RuleResponseAction) => { const endpointPrivileges = useUserPrivileges().endpointPrivileges; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/comment_field.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/comment_field.tsx index 1ae100103c76f..75e2039a1a480 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/comment_field.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/comment_field.tsx @@ -23,7 +23,7 @@ const CONFIG = { helpText: ( ), }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/field_name.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/field_name.tsx index 227d6a5d3329a..c713c23cfe65d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/field_name.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/field_name.tsx @@ -54,15 +54,21 @@ const FieldNameFieldComponent = ({ const renderEntityIdNote = useMemo(() => { const contains = fieldValue?.includes('entity_id'); + if (contains) { return ( ); } - return null; + return ( + + ); }, [fieldValue]); const CONFIG = useMemo(() => { @@ -80,7 +86,7 @@ const FieldNameFieldComponent = ({ 'xpack.securitySolution.responseActions.endpoint.validations.fieldNameIsRequiredErrorMessage', { defaultMessage: - '{field} is a required field when process.pid toggle is turned off', + '{field} selection is required when the process.pid toggle is disabled.', values: { field: FIELD_LABEL }, } ), diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/overwrite_process_field.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/overwrite_process_field.tsx index 143bd78e339d7..60b39842cdafc 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/overwrite_process_field.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/overwrite_process_field.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { ToggleField } from '@kbn/es-ui-shared-plugin/static/forms/components'; -import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; interface OverwriteFieldProps { path: string; @@ -23,9 +23,16 @@ const OverwriteFieldComponent = ({ const CONFIG = useMemo(() => { return { defaultValue: true, - label: i18n.translate('xpack.securitySolution.responseActions.endpoint.overwriteFieldLabel', { - defaultMessage: 'Use process.pid as process identifier', - }), + label: ( + process.pid, + }} + /> + ) as unknown as string, // in order to add a tag to the label, we need to cast element to string }; }, []); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/utils.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/utils.tsx index a07fd84bf4430..74e1c8d80cef3 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/utils.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/endpoint/utils.tsx @@ -7,7 +7,7 @@ import type { ReactNode } from 'react'; import React from 'react'; import { EuiText, EuiSpacer, EuiToolTip } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; +import { CONSOLE_COMMANDS } from '../../../management/common/translations'; import type { EnabledAutomatedResponseActionsCommands } from '../../../../common/endpoint/service/response_actions/constants'; interface EndpointActionTextProps { @@ -43,66 +43,21 @@ const useGetCommandText = ( switch (name) { case 'isolate': return { - title: ( - - ), - description: ( - - ), - tooltip: ( - - ), + title: CONSOLE_COMMANDS.isolate.title, + description: CONSOLE_COMMANDS.isolate.about, + tooltip: CONSOLE_COMMANDS.isolate.privileges, }; case 'kill-process': return { - title: ( - - ), - description: ( - - ), - tooltip: ( - - ), + title: CONSOLE_COMMANDS.killProcess.title, + description: CONSOLE_COMMANDS.killProcess.about, + tooltip: CONSOLE_COMMANDS.killProcess.privileges, }; case 'suspend-process': return { - title: ( - - ), - description: ( - - ), - tooltip: ( - - ), + title: CONSOLE_COMMANDS.suspendProcess.title, + description: CONSOLE_COMMANDS.suspendProcess.about, + tooltip: CONSOLE_COMMANDS.suspendProcess.privileges, }; default: return { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/get_supported_response_actions.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/get_supported_response_actions.ts index e8afdd91d1ff3..51b599028156d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/get_supported_response_actions.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/get_supported_response_actions.ts @@ -46,7 +46,7 @@ export const responseActionTypes: ResponseActionType[] = [ }, { id: ResponseActionTypesEnum['.endpoint'], - name: 'Endpoint Security', + name: 'Elastic Defend', iconClass: 'logoSecurity', }, ]; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/response_actions_form.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/response_actions_form.tsx index 31b5f16c1a037..b3f367ef675f6 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/response_actions_form.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/response_actions_form.tsx @@ -7,13 +7,14 @@ import React, { useEffect, useMemo, useState } from 'react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { map, reduce, upperFirst } from 'lodash'; +import { map, reduce } from 'lodash'; import ReactMarkdown from 'react-markdown'; import { ResponseActionsWrapper } from './response_actions_wrapper'; import { FORM_ERRORS_TITLE } from '../rule_creation/components/rule_actions_field/translations'; import { ResponseActionsHeader } from './response_actions_header'; import type { ArrayItem, FormHook } from '../../shared_imports'; import { useSupportedResponseActionTypes } from './use_supported_response_action_types'; +import { getActionDetails } from './constants'; interface ResponseActionsFormProps { items: ArrayItem[]; @@ -58,9 +59,9 @@ export const ResponseActionsForm = ({ if (name.includes(paramsPath)) { if (fields[name]?.errors?.length) { - const responseActionType = upperFirst( - (fields[`${path}.actionTypeId`].value as string).substring(1) - ); + const responseActionType = getActionDetails( + fields[`${path}.actionTypeId`].value as string + ).name; acc.push({ type: responseActionType, errors: map(fields[name].errors, 'message'), diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_badge.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_badge.tsx index a28e0e9e4df8a..036dcd4a6013b 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_badge.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_badge.tsx @@ -22,12 +22,14 @@ export const AssetCriticalityBadge: React.FC<{ criticalityLevel: CriticalityLevel; withDescription?: boolean; style?: React.CSSProperties; + className?: string; dataTestSubj?: string; }> = ({ criticalityLevel, style, dataTestSubj = 'asset-criticality-badge', withDescription = false, + className, }) => { const showDescription = withDescription ?? false; const badgeContent = showDescription ? ( @@ -46,6 +48,7 @@ export const AssetCriticalityBadge: React.FC<{ data-test-subj={dataTestSubj} color={CRITICALITY_LEVEL_COLOR[criticalityLevel]} style={style} + className={className} > {badgeContent} @@ -57,7 +60,8 @@ export const AssetCriticalityBadgeAllowMissing: React.FC<{ withDescription?: boolean; style?: React.CSSProperties; dataTestSubj?: string; -}> = ({ criticalityLevel, style, dataTestSubj, withDescription }) => { + className?: string; +}> = ({ criticalityLevel, style, dataTestSubj, withDescription, className }) => { if (criticalityLevel) { return ( ); } return ( - + ( + ({ eui: euiLightVars, darkMode: false })}>{storyFn()} +)); +const criticality = { + status: 'create', + query: {}, + privileges: {}, + mutation: {}, +} as State; + +const criticalityLoading = { + ...criticality, + query: { isLoading: true }, +} as State; + +export default { + component: AssetCriticalitySelector, + title: 'Components/AssetCriticalitySelector', +}; + +export const Default: Story = () => { + return ( + + +
+ +
+
+
+ ); +}; + +export const Compressed: Story = () => { + return ( + + +
+ +
+
+
+ ); +}; + +export const Loading: Story = () => { + return ( + + +
+ +
+
+
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.test.tsx new file mode 100644 index 0000000000000..691c240e651a5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.test.tsx @@ -0,0 +1,57 @@ +/* + * 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 { TestProviders } from '../../../common/mock'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { AssetCriticalitySelector } from './asset_criticality_selector'; +import type { State } from './use_asset_criticality'; + +const criticality = { + status: 'create', + query: {}, + privileges: { + data: { + has_write_permissions: true, + }, + }, + mutation: {}, +} as State; + +describe('AssetCriticalitySelector', () => { + it('renders', () => { + const { getByTestId } = render( + , + { + wrapper: TestProviders, + } + ); + + expect(getByTestId('asset-criticality-selector')).toBeInTheDocument(); + }); + + it('renders when compressed', () => { + const { getByTestId } = render( + , + { + wrapper: TestProviders, + } + ); + + expect(getByTestId('asset-criticality-change-btn')).toHaveAttribute( + 'aria-label', + 'Change asset criticality' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx index f6a0da49197c8..7bff57b119242 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/asset_criticality_selector.tsx @@ -6,7 +6,12 @@ */ import type { EuiSuperSelectOption } from '@elastic/eui'; + import { + EuiSpacer, + useEuiFontSize, + EuiButtonIcon, + useGeneratedHtmlId, EuiAccordion, EuiButton, EuiButtonEmpty, @@ -19,32 +24,122 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiSuperSelect, - EuiText, EuiTitle, EuiHorizontalRule, useEuiTheme, } from '@elastic/eui'; import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { css } from '@emotion/react'; +import { css } from '@emotion/css'; +import { i18n } from '@kbn/i18n'; +import { useToggle } from 'react-use'; import { PICK_ASSET_CRITICALITY } from './translations'; import { AssetCriticalityBadge, AssetCriticalityBadgeAllowMissing, } from './asset_criticality_badge'; -import type { Entity, ModalState, State } from './use_asset_criticality'; -import { useAssetCriticalityData, useCriticalityModal } from './use_asset_criticality'; +import type { Entity, State } from './use_asset_criticality'; +import { useAssetCriticalityData, useAssetCriticalityPrivileges } from './use_asset_criticality'; import type { CriticalityLevel } from '../../../../common/entity_analytics/asset_criticality/types'; interface Props { entity: Entity; } -const AssetCriticalityComponent: React.FC = ({ entity }) => { - const modal = useCriticalityModal(); - const criticality = useAssetCriticalityData(entity, modal); +const AssetCriticalitySelectorComponent: React.FC<{ + criticality: State; + entity: Entity; + compressed?: boolean; +}> = ({ criticality, entity, compressed = false }) => { + const [visible, toggleModal] = useToggle(false); + const sFontSize = useEuiFontSize('s').fontSize; + + return ( + <> + {criticality.query.isLoading || criticality.mutation.isLoading ? ( + <> + + + + ) : ( + + + + + {compressed && criticality.privileges.data?.has_write_permissions && ( + + toggleModal(true)} + /> + + )} + + {!compressed && criticality.privileges.data?.has_write_permissions && ( + + toggleModal(true)} + > + {criticality.status === 'update' ? ( + + ) : ( + + )} + + + )} + + )} + {visible ? ( + + ) : null} + + ); +}; + +export const AssetCriticalitySelector = React.memo(AssetCriticalitySelectorComponent); +AssetCriticalitySelector.displayName = 'AssetCriticalitySelector'; + +const AssetCriticalityAccordionComponent: React.FC = ({ entity }) => { const { euiTheme } = useEuiTheme(); + const privileges = useAssetCriticalityPrivileges(entity.name); + const criticality = useAssetCriticalityData({ + entity, + enabled: !!privileges.data?.has_read_permissions, + }); - if (criticality.privileges.isLoading || !criticality.privileges.data?.has_read_permissions) { + if (privileges.isLoading || !privileges.data?.has_read_permissions) { return null; } @@ -70,69 +165,27 @@ const AssetCriticalityComponent: React.FC = ({ entity }) => { }} data-test-subj="asset-criticality-selector" > - {criticality.query.isLoading || criticality.mutation.isLoading ? ( - - ) : ( - - - - - - - {criticality.privileges.data?.has_write_permissions && ( - - modal.toggle(true)} - > - {criticality.status === 'update' ? ( - - ) : ( - - )} - - - )} - - )} + - {modal.visible ? ( - - ) : null} ); }; interface ModalProps { criticality: State; - modal: ModalState; + toggle: (nextValue: boolean) => void; entity: Entity; } -const AssetCriticalityModal: React.FC = ({ criticality, modal, entity }) => { + +const AssetCriticalityModal: React.FC = ({ criticality, entity, toggle }) => { + const basicSelectId = useGeneratedHtmlId({ prefix: 'basicSelect' }); const [value, setNewValue] = useState( criticality.query.data?.criticality_level ?? 'normal' ); return ( - modal.toggle(false)}> + toggle(false)}> {PICK_ASSET_CRITICALITY} @@ -140,7 +193,7 @@ const AssetCriticalityModal: React.FC = ({ criticality, modal, entit = ({ criticality, modal, entit /> - modal.toggle(false)}> + toggle(false)}> = ({ criticality, modal, entit + onClick={() => { criticality.mutation.mutate({ criticalityLevel: value, idField: `${entity.type}.name`, idValue: entity.name, - }) - } + }); + toggle(false); + }} fill data-test-subj="asset-criticality-modal-save-btn" > @@ -198,5 +252,5 @@ const options: Array> = [ option('very_important'), ]; -export const AssetCriticalitySelector = React.memo(AssetCriticalityComponent); -AssetCriticalitySelector.displayName = 'AssetCriticalitySelector'; +export const AssetCriticalityAccordion = React.memo(AssetCriticalityAccordionComponent); +AssetCriticalityAccordion.displayName = 'AssetCriticalityAccordion'; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/use_asset_criticality.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/use_asset_criticality.ts index 893f157d045cf..1d43ec6dacde4 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/use_asset_criticality.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality/use_asset_criticality.ts @@ -5,49 +5,50 @@ * 2.0. */ -import { useGeneratedHtmlId } from '@elastic/eui'; import type { UseMutationResult, UseQueryResult } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; - -import { useToggle } from 'react-use'; import type { AssetCriticalityRecord } from '../../../../common/api/entity_analytics/asset_criticality'; import type { EntityAnalyticsPrivileges } from '../../../../common/api/entity_analytics/common'; import type { AssetCriticality } from '../../api/api'; import { useEntityAnalyticsRoutes } from '../../api/api'; -// SUGGESTION: @tiansivive Move this to some more general place within Entity Analytics -export const buildCriticalityQueryKeys = (id: string) => { - const ASSET_CRITICALITY = 'ASSET_CRITICALITY'; - const PRIVILEGES = 'PRIVILEGES'; - return { - doc: [ASSET_CRITICALITY, id], - privileges: [ASSET_CRITICALITY, PRIVILEGES, id], - }; -}; - -export const useAssetCriticalityData = (entity: Entity, modal: ModalState): State => { - const QC = useQueryClient(); - const QUERY_KEYS = buildCriticalityQueryKeys(entity.name); +const ASSET_CRITICALITY_KEY = 'ASSET_CRITICALITY'; +const PRIVILEGES_KEY = 'PRIVILEGES'; - const { fetchAssetCriticality, createAssetCriticality, fetchAssetCriticalityPrivileges } = - useEntityAnalyticsRoutes(); +export const useAssetCriticalityPrivileges = ( + entityName: string +): UseQueryResult => { + const { fetchAssetCriticalityPrivileges } = useEntityAnalyticsRoutes(); - const privileges = useQuery({ - queryKey: QUERY_KEYS.privileges, + return useQuery({ + queryKey: [ASSET_CRITICALITY_KEY, PRIVILEGES_KEY, entityName], queryFn: fetchAssetCriticalityPrivileges, }); +}; + +export const useAssetCriticalityData = ({ + entity, + enabled = true, +}: { + entity: Entity; + enabled?: boolean; +}): State => { + const QC = useQueryClient(); + const QUERY_KEY = [ASSET_CRITICALITY_KEY, entity.name]; + const { fetchAssetCriticality, createAssetCriticality } = useEntityAnalyticsRoutes(); + + const privileges = useAssetCriticalityPrivileges(entity.name); const query = useQuery({ - queryKey: QUERY_KEYS.doc, + queryKey: QUERY_KEY, queryFn: () => fetchAssetCriticality({ idField: `${entity.type}.name`, idValue: entity.name }), retry: (failureCount, error) => error.body.statusCode === 404 && failureCount > 0, - enabled: !!privileges.data?.has_read_permissions, + enabled, }); const mutation = useMutation({ mutationFn: createAssetCriticality, onSuccess: (data) => { - QC.setQueryData(QUERY_KEYS.doc, data); - modal.toggle(false); + QC.setQueryData(QUERY_KEY, data); }, }); @@ -59,12 +60,6 @@ export const useAssetCriticalityData = (entity: Entity, modal: ModalState): Stat }; }; -export const useCriticalityModal = () => { - const [visible, toggle] = useToggle(false); - const basicSelectId = useGeneratedHtmlId({ prefix: 'basicSelect' }); - return { visible, toggle, basicSelectId }; -}; - export interface State { status: 'create' | 'update'; query: UseQueryResult; @@ -74,7 +69,6 @@ export interface State { type Params = Pick; export interface ModalState { - basicSelectId: string; visible: boolean; toggle: (next: boolean) => void; } diff --git a/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx index 002a1b24eb0a8..3bf714efaf866 100644 --- a/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx @@ -10,6 +10,7 @@ import { EuiFlexItem, EuiHorizontalRule, EuiSpacer, + EuiTitle, EuiWindowEvent, } from '@elastic/eui'; import { noop } from 'lodash/fp'; @@ -20,6 +21,12 @@ import type { Filter } from '@kbn/es-query'; import { buildEsQuery } from '@kbn/es-query'; import { getEsQueryConfig } from '@kbn/data-plugin/common'; import { tableDefaults, dataTableSelectors, TableId } from '@kbn/securitysolution-data-table'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + useAssetCriticalityData, + useAssetCriticalityPrivileges, +} from '../../../../entity_analytics/components/asset_criticality/use_asset_criticality'; +import { AssetCriticalitySelector } from '../../../../entity_analytics/components/asset_criticality/asset_criticality_selector'; import { AlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { useAlertsPrivileges } from '../../../../detections/containers/detection_engine/alerts/use_alerts_privileges'; @@ -167,6 +174,14 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta [detailName] ); + const entity = useMemo(() => ({ type: 'host' as const, name: detailName }), [detailName]); + const privileges = useAssetCriticalityPrivileges(entity.name); + const canReadAssetCriticality = !!privileges.data?.has_read_permissions; + const criticality = useAssetCriticalityData({ + entity, + enabled: canReadAssetCriticality, + }); + return ( <> {indicesExist ? ( @@ -199,7 +214,21 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta ), ]} /> - + {canReadAssetCriticality && ( + <> + +

+ +

+
+ + + + + )} = ({ [detailName] ); + const entity = useMemo(() => ({ type: 'user' as const, name: detailName }), [detailName]); + const privileges = useAssetCriticalityPrivileges(entity.name); + const canReadAssetCriticality = !!privileges.data?.has_read_permissions; + const criticality = useAssetCriticalityData({ + entity, + enabled: canReadAssetCriticality, + }); + return ( <> {indicesExist ? ( @@ -191,6 +206,23 @@ const UsersDetailsComponent: React.FC = ({ title={detailName} /> + {canReadAssetCriticality && ( + <> + + +

+ +

+
+ + + + + )} + )} - + )} - + { values: { error: error.message }, }); }; + +export const CONSOLE_COMMANDS = { + isolate: { + title: i18n.translate('xpack.securitySolution.endpointConsoleCommands.isolate.title', { + defaultMessage: 'Isolate', + }), + about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.isolate.about', { + defaultMessage: 'Isolate the host', + }), + privileges: i18n.translate( + 'xpack.securitySolution.endpointConsoleCommands.isolate.privileges', + { + defaultMessage: + 'Insufficient privileges to isolate hosts. Contact your Kibana administrator if you think you should have this permission.', + } + ), + }, + release: { + about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.release.about', { + defaultMessage: 'Release the host', + }), + }, + killProcess: { + title: i18n.translate('xpack.securitySolution.endpointConsoleCommands.killProcess.title', { + defaultMessage: 'Kill process', + }), + about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.killProcess.about', { + defaultMessage: 'Kill/terminate a process', + }), + privileges: i18n.translate( + 'xpack.securitySolution.endpointConsoleCommands.killProcess.privileges', + { + defaultMessage: + 'Insufficient privileges to kill process. Contact your Kibana administrator if you think you should have this permission.', + } + ), + args: { + pid: { + about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.pid.arg.comment', { + defaultMessage: 'A PID representing the process to kill', + }), + }, + entityId: { + about: i18n.translate( + 'xpack.securitySolution.endpointConsoleCommands.entityId.arg.comment', + { + defaultMessage: 'An entity id representing the process to kill', + } + ), + }, + }, + }, + suspendProcess: { + title: i18n.translate('xpack.securitySolution.endpointConsoleCommands.suspendProcess.title', { + defaultMessage: 'Suspend process', + }), + about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.suspendProcess.about', { + defaultMessage: 'Temporarily suspend a process', + }), + privileges: i18n.translate( + 'xpack.securitySolution.endpointConsoleCommands.suspendProcess.privileges', + { + defaultMessage: + 'Insufficient privileges to supend process. Contact your Kibana administrator if you think you should have this permission.', + } + ), + args: { + pid: { + about: i18n.translate( + 'xpack.securitySolution.endpointConsoleCommands.suspendProcess.pid.arg.comment', + { + defaultMessage: 'A PID representing the process to suspend', + } + ), + }, + entityId: { + about: i18n.translate( + 'xpack.securitySolution.endpointConsoleCommands.suspendProcess.entityId.arg.comment', + { + defaultMessage: 'An entity id representing the process to suspend', + } + ), + }, + }, + }, + status: { + about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.status.about', { + defaultMessage: 'Show host status information', + }), + }, + processes: { + about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.processes.about', { + defaultMessage: 'Show all running processes', + }), + }, + getFile: { + about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.getFile.about', { + defaultMessage: 'Retrieve a file from the host', + }), + args: { + path: { + about: i18n.translate( + 'xpack.securitySolution.endpointConsoleCommands.getFile.pathArgAbout', + { + defaultMessage: 'The full file path to be retrieved', + } + ), + }, + }, + }, + execute: { + about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.execute.about', { + defaultMessage: 'Execute a command on the host', + }), + args: { + timeout: { + about: i18n.translate( + 'xpack.securitySolution.endpointConsoleCommands.execute.args.timeout.about', + { + defaultMessage: + 'The timeout in units of time (h for hours, m for minutes, s for seconds) for the endpoint to wait for the script to complete. Example: 37m. If not given, it defaults to 4 hours.', + } + ), + }, + }, + }, + upload: { + about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.upload.about', { + defaultMessage: 'Upload a file to the host', + }), + args: { + file: { + about: i18n.translate( + 'xpack.securitySolution.endpointConsoleCommands.upload.args.file.about', + { + defaultMessage: 'The file that will be sent to the host', + } + ), + }, + overwrite: { + about: i18n.translate( + 'xpack.securitySolution.endpointConsoleCommands.upload.args.overwrite.about', + { + defaultMessage: 'Overwrite the file on the host if it already exists', + } + ), + }, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts index 9e429ab6a383c..238efec7542dc 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts @@ -37,6 +37,7 @@ import { import { getCommandAboutInfo } from './get_command_about_info'; import { validateUnitOfTime } from './utils'; +import { CONSOLE_COMMANDS } from '../../../common/translations'; const emptyArgumentValidator = (argData: ParsedArgData): true | string => { if (argData?.length > 0 && typeof argData[0] === 'string' && argData[0]?.trim().length > 0) { @@ -154,9 +155,7 @@ export const getEndpointConsoleCommands = ({ { name: 'isolate', about: getCommandAboutInfo({ - aboutInfo: i18n.translate('xpack.securitySolution.endpointConsoleCommands.isolate.about', { - defaultMessage: 'Isolate the host', - }), + aboutInfo: CONSOLE_COMMANDS.isolate.about, isSupported: doesEndpointSupportCommand('isolate'), }), RenderComponent: IsolateActionResult, @@ -185,9 +184,8 @@ export const getEndpointConsoleCommands = ({ { name: 'release', about: getCommandAboutInfo({ - aboutInfo: i18n.translate('xpack.securitySolution.endpointConsoleCommands.release.about', { - defaultMessage: 'Release the host', - }), + aboutInfo: CONSOLE_COMMANDS.release.about, + isSupported: doesEndpointSupportCommand('release'), }), RenderComponent: ReleaseActionResult, @@ -214,14 +212,10 @@ export const getEndpointConsoleCommands = ({ helpHidden: !getRbacControl({ commandName: 'release', privileges: endpointPrivileges }), }, { + // name: 'kill-process', about: getCommandAboutInfo({ - aboutInfo: i18n.translate( - 'xpack.securitySolution.endpointConsoleCommands.killProcess.about', - { - defaultMessage: 'Kill/terminate a process', - } - ), + aboutInfo: CONSOLE_COMMANDS.killProcess.about, isSupported: doesEndpointSupportCommand('kill-process'), }), RenderComponent: KillProcessActionResult, @@ -245,21 +239,14 @@ export const getEndpointConsoleCommands = ({ required: false, allowMultiples: false, exclusiveOr: true, - about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.pid.arg.comment', { - defaultMessage: 'A PID representing the process to kill', - }), + about: CONSOLE_COMMANDS.killProcess.args.pid.about, validate: pidValidator, }, entityId: { required: false, allowMultiples: false, exclusiveOr: true, - about: i18n.translate( - 'xpack.securitySolution.endpointConsoleCommands.entityId.arg.comment', - { - defaultMessage: 'An entity id representing the process to kill', - } - ), + about: CONSOLE_COMMANDS.killProcess.args.entityId.about, validate: emptyArgumentValidator, }, }, @@ -272,12 +259,7 @@ export const getEndpointConsoleCommands = ({ { name: 'suspend-process', about: getCommandAboutInfo({ - aboutInfo: i18n.translate( - 'xpack.securitySolution.endpointConsoleCommands.suspendProcess.about', - { - defaultMessage: 'Temporarily suspend a process', - } - ), + aboutInfo: CONSOLE_COMMANDS.suspendProcess.about, isSupported: doesEndpointSupportCommand('suspend-process'), }), RenderComponent: SuspendProcessActionResult, @@ -301,24 +283,14 @@ export const getEndpointConsoleCommands = ({ required: false, allowMultiples: false, exclusiveOr: true, - about: i18n.translate( - 'xpack.securitySolution.endpointConsoleCommands.suspendProcess.pid.arg.comment', - { - defaultMessage: 'A PID representing the process to suspend', - } - ), + about: CONSOLE_COMMANDS.suspendProcess.args.pid.about, validate: pidValidator, }, entityId: { required: false, allowMultiples: false, exclusiveOr: true, - about: i18n.translate( - 'xpack.securitySolution.endpointConsoleCommands.suspendProcess.entityId.arg.comment', - { - defaultMessage: 'An entity id representing the process to suspend', - } - ), + about: CONSOLE_COMMANDS.suspendProcess.args.entityId.about, validate: emptyArgumentValidator, }, }, @@ -333,9 +305,7 @@ export const getEndpointConsoleCommands = ({ }, { name: 'status', - about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.status.about', { - defaultMessage: 'Show host status information', - }), + about: CONSOLE_COMMANDS.status.about, RenderComponent: EndpointStatusActionResult, meta: { endpointId: endpointAgentId, @@ -347,12 +317,7 @@ export const getEndpointConsoleCommands = ({ { name: 'processes', about: getCommandAboutInfo({ - aboutInfo: i18n.translate( - 'xpack.securitySolution.endpointConsoleCommands.processes.about', - { - defaultMessage: 'Show all running processes', - } - ), + aboutInfo: CONSOLE_COMMANDS.processes.about, isSupported: doesEndpointSupportCommand('processes'), }), RenderComponent: GetProcessesActionResult, @@ -381,9 +346,7 @@ export const getEndpointConsoleCommands = ({ { name: 'get-file', about: getCommandAboutInfo({ - aboutInfo: i18n.translate('xpack.securitySolution.endpointConsoleCommands.getFile.about', { - defaultMessage: 'Retrieve a file from the host', - }), + aboutInfo: CONSOLE_COMMANDS.getFile.about, isSupported: doesEndpointSupportCommand('processes'), }), RenderComponent: GetFileActionResult, @@ -401,12 +364,7 @@ export const getEndpointConsoleCommands = ({ path: { required: true, allowMultiples: false, - about: i18n.translate( - 'xpack.securitySolution.endpointConsoleCommands.getFile.pathArgAbout', - { - defaultMessage: 'The full file path to be retrieved', - } - ), + about: CONSOLE_COMMANDS.getFile.args.path.about, validate: (argData) => { return emptyArgumentValidator(argData); }, @@ -429,9 +387,7 @@ export const getEndpointConsoleCommands = ({ { name: 'execute', about: getCommandAboutInfo({ - aboutInfo: i18n.translate('xpack.securitySolution.endpointConsoleCommands.execute.about', { - defaultMessage: 'Execute a command on the host', - }), + aboutInfo: CONSOLE_COMMANDS.execute.about, isSupported: doesEndpointSupportCommand('execute'), }), RenderComponent: ExecuteActionResult, @@ -455,13 +411,7 @@ export const getEndpointConsoleCommands = ({ timeout: { required: false, allowMultiples: false, - about: i18n.translate( - 'xpack.securitySolution.endpointConsoleCommands.execute.args.timeout.about', - { - defaultMessage: - 'The timeout in units of time (h for hours, m for minutes, s for seconds) for the endpoint to wait for the script to complete. Example: 37m. If not given, it defaults to 4 hours.', - } - ), + about: CONSOLE_COMMANDS.execute.args.timeout.about, mustHaveValue: 'non-empty-string', validate: executeTimeoutValidator, }, @@ -488,9 +438,7 @@ export const getEndpointConsoleCommands = ({ consoleCommands.push({ name: 'upload', about: getCommandAboutInfo({ - aboutInfo: i18n.translate('xpack.securitySolution.endpointConsoleCommands.upload.about', { - defaultMessage: 'Upload a file to the host', - }), + aboutInfo: CONSOLE_COMMANDS.upload.about, isSupported: doesEndpointSupportCommand('upload'), }), RenderComponent: UploadActionResult, @@ -508,24 +456,14 @@ export const getEndpointConsoleCommands = ({ file: { required: true, allowMultiples: false, - about: i18n.translate( - 'xpack.securitySolution.endpointConsoleCommands.upload.args.file.about', - { - defaultMessage: 'The file that will be sent to the host', - } - ), + about: CONSOLE_COMMANDS.upload.args.file.about, mustHaveValue: 'truthy', SelectorComponent: ArgumentFileSelector, }, overwrite: { required: false, allowMultiples: false, - about: i18n.translate( - 'xpack.securitySolution.endpointConsoleCommands.upload.args.overwrite.about', - { - defaultMessage: 'Overwrite the file on the host if it already exists', - } - ), + about: CONSOLE_COMMANDS.upload.args.overwrite.about, mustHaveValue: false, }, comment: { diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/form.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/form.cy.ts index 4fa0bb3b0c2e0..770be07a61257 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/form.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/form.cy.ts @@ -119,7 +119,7 @@ describe( cy.getByTestSubj(RESPONSE_ACTIONS_ERRORS).within(() => { cy.contains( - 'Custom field name is a required field when process.pid toggle is turned off' + 'Custom field name selection is required when the process.pid toggle is disabled.' ); }); @@ -208,7 +208,7 @@ describe( it('response actions are disabled', () => { fillUpNewRule(ruleName, ruleDescription); cy.getByTestSubj('response-actions-wrapper').within(() => { - cy.getByTestSubj('Endpoint Security-response-action-type-selection-option').should( + cy.getByTestSubj('Elastic Defend-response-action-type-selection-option').should( 'be.disabled' ); }); @@ -234,7 +234,7 @@ describe( cy.getByTestSubj('edit-rule-actions-tab').click(); cy.getByTestSubj('response-actions-wrapper').within(() => { - cy.getByTestSubj('Endpoint Security-response-action-type-selection-option').should( + cy.getByTestSubj('Elastic Defend-response-action-type-selection-option').should( 'be.disabled' ); }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/no_license.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/no_license.cy.ts index d120459b64ea3..a427413429b19 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/no_license.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/no_license.cy.ts @@ -25,7 +25,7 @@ describe('No License', { tags: '@ess', env: { ftrConfig: { license: 'basic' } } it('response actions are disabled', () => { fillUpNewRule(ruleName, ruleDescription); cy.getByTestSubj('response-actions-wrapper').within(() => { - cy.getByTestSubj('Endpoint Security-response-action-type-selection-option').should( + cy.getByTestSubj('Elastic Defend-response-action-type-selection-option').should( 'be.disabled' ); }); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts index a03d5a2ed483d..488742ac945c8 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/response_actions.ts @@ -43,7 +43,7 @@ export const validateAvailableCommands = () => { }; export const addEndpointResponseAction = () => { cy.getByTestSubj('response-actions-wrapper').within(() => { - cy.getByTestSubj('Endpoint Security-response-action-type-selection-option').click(); + cy.getByTestSubj('Elastic Defend-response-action-type-selection-option').click(); }); }; export const focusAndOpenCommandDropdown = (number = 0) => { @@ -99,12 +99,10 @@ export const getRunningProcesses = (command: string): Cypress.Chainable export const tryAddingDisabledResponseAction = (itemNumber = 0) => { cy.getByTestSubj('response-actions-wrapper').within(() => { - cy.getByTestSubj('Endpoint Security-response-action-type-selection-option').should( - 'be.disabled' - ); + cy.getByTestSubj('Elastic Defend-response-action-type-selection-option').should('be.disabled'); }); // Try adding new action, should not add list item. - cy.getByTestSubj('Endpoint Security-response-action-type-selection-option').click({ + cy.getByTestSubj('Elastic Defend-response-action-type-selection-option').click({ force: true, }); cy.getByTestSubj(`response-actions-list-item-${itemNumber}`).should('not.exist'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/index.tsx index 1f214edf61b2c..29c9c2019a4d2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/index.tsx @@ -17,7 +17,7 @@ import { import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { AssetCriticalitySelector } from '../../../../entity_analytics/components/asset_criticality/asset_criticality_selector'; +import { AssetCriticalityAccordion } from '../../../../entity_analytics/components/asset_criticality/asset_criticality_selector'; import { ExpandableHostDetails, ExpandableHostDetailsPageLink, @@ -83,7 +83,7 @@ export const HostDetailsPanel: React.FC = React.memo( - + @@ -111,7 +111,7 @@ export const HostDetailsPanel: React.FC = React.memo( - + - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_side_panel.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_side_panel.tsx index 1f52f65a899f9..2223b5cc04f6a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_side_panel.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/user_details/user_details_side_panel.tsx @@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiSpacer } from '@elastic/eu import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { AssetCriticalitySelector } from '../../../../entity_analytics/components/asset_criticality/asset_criticality_selector'; +import { AssetCriticalityAccordion } from '../../../../entity_analytics/components/asset_criticality/asset_criticality_selector'; import { ExpandableUserDetailsTitle, ExpandableUserDetailsPageLink, @@ -68,7 +68,7 @@ export const UserDetailsSidePanel = ({ - + { - let esClient: ElasticsearchClient; let fleetServices: EndpointInternalFleetServicesInterface; let appFeatureService: AppFeaturesService; let logger: Logger; const callTurnOffAgentPolicyFeatures = () => - turnOffAgentPolicyFeatures(esClient, fleetServices, appFeatureService, logger); + turnOffAgentPolicyFeatures(fleetServices, appFeatureService, logger); beforeEach(() => { const endpointContextStartContract = createMockEndpointAppContextServiceStartContract(); - ({ esClient, logger } = endpointContextStartContract); + ({ logger } = endpointContextStartContract); appFeatureService = endpointContextStartContract.appFeaturesService; fleetServices = endpointContextStartContract.endpointFleetServicesFactory.asInternalUser(); @@ -39,7 +35,9 @@ describe('Turn Off Agent Policy Features Migration', () => { it('should do nothing', async () => { await callTurnOffAgentPolicyFeatures(); - expect(fleetServices.agentPolicy.list as jest.Mock).not.toHaveBeenCalled(); + expect( + fleetServices.agentPolicy.turnOffAgentTamperProtections as jest.Mock + ).not.toHaveBeenCalled(); expect(logger.info).toHaveBeenLastCalledWith( 'App feature [endpoint_agent_tamper_protection] is enabled. Nothing to do!' ); @@ -47,121 +45,55 @@ describe('Turn Off Agent Policy Features Migration', () => { }); describe('and `agentTamperProtection` is disabled', () => { - let policyGenerator: FleetAgentPolicyGenerator; - let page1Items: GetAgentPoliciesResponseItem[] = []; - let page2Items: GetAgentPoliciesResponseItem[] = []; - let page3Items: GetAgentPoliciesResponseItem[] = []; - let bulkUpdateResponse: AgentPolicy[]; - - const generatePolicyMock = (): GetAgentPoliciesResponseItem => { - return policyGenerator.generate({ is_protected: true }); - }; - beforeEach(() => { - policyGenerator = new FleetAgentPolicyGenerator('seed'); - const agentPolicyListSrv = fleetServices.agentPolicy.list as jest.Mock; - appFeatureService = createAppFeaturesServiceMock( ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_agent_tamper_protection') ); - - page1Items = [generatePolicyMock(), generatePolicyMock()]; - page2Items = [generatePolicyMock(), generatePolicyMock()]; - page3Items = [generatePolicyMock()]; - - agentPolicyListSrv - .mockImplementationOnce(async () => { - return { - total: 2500, - page: 1, - perPage: 1000, - items: page1Items, - }; - }) - .mockImplementationOnce(async () => { - return { - total: 2500, - page: 2, - perPage: 1000, - items: page2Items, - }; - }) - .mockImplementationOnce(async () => { - return { - total: 2500, - page: 3, - perPage: 1000, - items: page3Items, - }; - }); - - bulkUpdateResponse = [ - page1Items[0], - page1Items[1], - page2Items[0], - page2Items[1], - page3Items[0], - ]; - - (fleetServices.agentPolicy.bumpRevision as jest.Mock).mockImplementation(async () => { - return bulkUpdateResponse; - }); }); - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should update only policies that have protections turn on', async () => { + it('should log proper message if all agent policies are already protected', async () => { + (fleetServices.agentPolicy.turnOffAgentTamperProtections as jest.Mock).mockResolvedValueOnce({ + updatedPolicies: null, + failedPolicies: [], + }); await callTurnOffAgentPolicyFeatures(); + expect(logger.info).toHaveBeenCalledWith('All agent policies are compliant, nothing to do!'); + }); - expect(fleetServices.agentPolicy.list as jest.Mock).toHaveBeenCalledTimes(3); - - const updates = Array.from({ length: 5 }, (_, i) => ({ - soClient: fleetServices.internalSoClient, - esClient, - id: bulkUpdateResponse![i].id, - })); - - expect(fleetServices.agentPolicy.bumpRevision as jest.Mock).toHaveBeenCalledTimes(5); - updates.forEach((args, i) => { - expect(fleetServices.agentPolicy.bumpRevision as jest.Mock).toHaveBeenNthCalledWith( - i + 1, - args.soClient, - args.esClient, - args.id, - { removeProtection: true, user: { username: 'elastic' } } - ); + it('should log proper message if all agent policies are updated successfully', async () => { + (fleetServices.agentPolicy.turnOffAgentTamperProtections as jest.Mock).mockResolvedValueOnce({ + updatedPolicies: [{ id: 'policy 1' }, { id: 'policy 2' }], + failedPolicies: [], }); - - expect(logger.info).toHaveBeenCalledWith( - 'App feature [endpoint_agent_tamper_protection] is disabled. Checking fleet agent policies for compliance' + await callTurnOffAgentPolicyFeatures(); + expect(logger.info).toHaveBeenLastCalledWith( + 'Done - 2 out of 2 were successful. No errors encountered.' ); + }); - expect(logger.info).toHaveBeenCalledWith( - `Found 5 policies that need updates:\n${bulkUpdateResponse! - .map( - (policy) => - `Policy [${policy.id}][${policy.name}] updated to disable agent tamper protection.` - ) - .join('\n')}` + it('should log proper message if all agent policies fail to update', async () => { + (fleetServices.agentPolicy.turnOffAgentTamperProtections as jest.Mock).mockResolvedValueOnce({ + updatedPolicies: null, + failedPolicies: [ + { id: 'policy1', error: 'error1' }, + { id: 'policy2', error: 'error2' }, + ], + }); + await callTurnOffAgentPolicyFeatures(); + expect(logger.error).toHaveBeenLastCalledWith( + 'Done - all 2 failed to update. Errors encountered:\nPolicy [policy1] failed to update due to error: error1\nPolicy [policy2] failed to update due to error: error2' ); - expect(logger.info).toHaveBeenCalledWith('Done. All updates applied successfully'); }); - it('should log failures', async () => { - (fleetServices.agentPolicy.bumpRevision as jest.Mock).mockImplementationOnce(async () => { - throw new Error('oh noo'); + it('should log proper message if some agent policies fail to update', async () => { + (fleetServices.agentPolicy.turnOffAgentTamperProtections as jest.Mock).mockResolvedValueOnce({ + updatedPolicies: [{ id: 'policy3' }], + failedPolicies: [{ id: 'policy1', error: 'error1' }], }); await callTurnOffAgentPolicyFeatures(); - - expect(logger.error).toHaveBeenCalledWith( - `Done - 1 out of 5 were successful. Errors encountered:\nPolicy [${ - bulkUpdateResponse![0].id - }] failed to update due to error: Error: oh noo` + expect(logger.error).toHaveBeenLastCalledWith( + 'Done - 1 out of 2 were successful. Errors encountered:\nPolicy [policy1] failed to update due to error: error1' ); - - expect(fleetServices.agentPolicy.bumpRevision as jest.Mock).toHaveBeenCalledTimes(5); }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.ts b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.ts index 0d1b01d68b765..01994a683bb92 100644 --- a/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.ts +++ b/x-pack/plugins/security_solution/server/endpoint/migrations/turn_off_agent_policy_features.ts @@ -5,16 +5,12 @@ * 2.0. */ -import type { Logger, ElasticsearchClient } from '@kbn/core/server'; -import type { AgentPolicy } from '@kbn/fleet-plugin/common'; -import type { AuthenticatedUser } from '@kbn/security-plugin/common'; +import type { Logger } from '@kbn/core/server'; import { AppFeatureSecurityKey } from '@kbn/security-solution-features/keys'; -import pMap from 'p-map'; import type { EndpointInternalFleetServicesInterface } from '../services/fleet'; import type { AppFeaturesService } from '../../lib/app_features_service/app_features_service'; export const turnOffAgentPolicyFeatures = async ( - esClient: ElasticsearchClient, fleetServices: EndpointInternalFleetServicesInterface, appFeaturesService: AppFeaturesService, logger: Logger @@ -34,59 +30,29 @@ export const turnOffAgentPolicyFeatures = async ( ); const { agentPolicy: agentPolicyService, internalSoClient } = fleetServices; - const updates: AgentPolicy[] = []; - const messages: string[] = []; - const perPage = 1000; - let hasMoreData = true; - let total = 0; - let page = 1; - - do { - const currentPage = page++; - const { items, total: totalPolicies } = await agentPolicyService.list(internalSoClient, { - page: currentPage, - kuery: 'ingest-agent-policies.is_protected: true', - perPage, - }); - - total = totalPolicies; - hasMoreData = currentPage * perPage < total; - - for (const item of items) { - messages.push( - `Policy [${item.id}][${item.name}] updated to disable agent tamper protection.` - ); - - updates.push({ ...item, is_protected: false }); - } - } while (hasMoreData); - - if (updates.length > 0) { - logger.info(`Found ${updates.length} policies that need updates:\n${messages.join('\n')}`); - const policyUpdateErrors: Array<{ id: string; error: Error }> = []; - await pMap(updates, async (update) => { - try { - return await agentPolicyService.bumpRevision(internalSoClient, esClient, update.id, { - user: { username: 'elastic' } as AuthenticatedUser, - removeProtection: true, - }); - } catch (error) { - policyUpdateErrors.push({ error, id: update.id }); - } - }); - - if (policyUpdateErrors.length > 0) { - logger.error( - `Done - ${policyUpdateErrors.length} out of ${ - updates.length - } were successful. Errors encountered:\n${policyUpdateErrors - .map((e) => `Policy [${e.id}] failed to update due to error: ${e.error}`) - .join('\n')}` - ); - } else { - logger.info(`Done. All updates applied successfully`); - } + const { updatedPolicies, failedPolicies } = + await agentPolicyService.turnOffAgentTamperProtections(internalSoClient); + + if (!updatedPolicies && !failedPolicies.length) { + log.info(`All agent policies are compliant, nothing to do!`); + } else if (updatedPolicies && failedPolicies.length) { + const totalPolicies = updatedPolicies.length + failedPolicies.length; + logger.error( + `Done - ${ + failedPolicies.length + } out of ${totalPolicies} were successful. Errors encountered:\n${failedPolicies + .map((e) => `Policy [${e.id}] failed to update due to error: ${e.error}`) + .join('\n')}` + ); + } else if (updatedPolicies) { + logger.info( + `Done - ${updatedPolicies.length} out of ${updatedPolicies.length} were successful. No errors encountered.` + ); } else { - logger.info(`Done. Checked ${total} policies and no updates needed`); + logger.error( + `Done - all ${failedPolicies.length} failed to update. Errors encountered:\n${failedPolicies + .map((e) => `Policy [${e.id}] failed to update due to error: ${e.error}`) + .join('\n')}` + ); } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/upgrade_prebuilt_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/upgrade_prebuilt_rules.test.ts index 2990533dc0f89..f4845be99c68e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/upgrade_prebuilt_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/upgrade_prebuilt_rules.test.ts @@ -5,17 +5,23 @@ * 2.0. */ +import { omit } from 'lodash'; import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; import { getRuleMock, getFindResultWithSingleHit, + getFindResultWithMultiHits, } from '../../../routes/__mocks__/request_responses'; import { upgradePrebuiltRules } from './upgrade_prebuilt_rules'; import { patchRules } from '../../../rule_management/logic/crud/patch_rules'; +import { createRules } from '../../../rule_management/logic/crud/create_rules'; +import { deleteRules } from '../../../rule_management/logic/crud/delete_rules'; import { getPrebuiltRuleMock, getPrebuiltThreatMatchRuleMock } from '../../mocks'; -import { getThreatRuleParams } from '../../../rule_schema/mocks'; +import { getQueryRuleParams, getThreatRuleParams } from '../../../rule_schema/mocks'; jest.mock('../../../rule_management/logic/crud/patch_rules'); +jest.mock('../../../rule_management/logic/crud/create_rules'); +jest.mock('../../../rule_management/logic/crud/delete_rules'); describe('updatePrebuiltRules', () => { let rulesClient: ReturnType; @@ -24,35 +30,173 @@ describe('updatePrebuiltRules', () => { rulesClient = rulesClientMock.create(); }); - it('should omit actions and enabled when calling patchRules', async () => { + describe('when upgrading a prebuilt rule to a newer version with the same rule type', () => { + const prepackagedRule = getPrebuiltRuleMock({ + rule_id: 'rule-to-upgrade', + }); + + beforeEach(() => { + const installedRule = getRuleMock( + getQueryRuleParams({ + ruleId: 'rule-to-upgrade', + }) + ); + + rulesClient.find.mockResolvedValue( + getFindResultWithMultiHits({ + data: [installedRule], + }) + ); + }); + + it('patches existing rule with incoming version data', async () => { + await upgradePrebuiltRules(rulesClient, [prepackagedRule]); + + expect(patchRules).toHaveBeenCalledWith( + expect.objectContaining({ + nextParams: expect.objectContaining(prepackagedRule), + }) + ); + }); + + it('makes sure enabled state is preserved', async () => { + await upgradePrebuiltRules(rulesClient, [prepackagedRule]); + + expect(patchRules).toHaveBeenCalledWith( + expect.objectContaining({ + nextParams: expect.objectContaining({ + enabled: undefined, + }), + }) + ); + }); + }); + + describe('when upgrading a prebuilt rule to a newer version with a different rule type', () => { + const prepackagedRule = getPrebuiltRuleMock({ + rule_id: 'rule-to-upgrade', + type: 'eql', + language: 'eql', + query: 'host where host.name == "something"', + }); const actions = [ { group: 'group', id: 'id', - action_type_id: 'action_type_id', + actionTypeId: 'action_type_id', params: {}, }, ]; - const prepackagedRule = getPrebuiltRuleMock(); - rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); + const installedRule = getRuleMock( + getQueryRuleParams({ + ruleId: 'rule-to-upgrade', + type: 'query', + exceptionsList: [ + { + id: 'exception_list_1', + list_id: 'exception_list_1', + namespace_type: 'agnostic', + type: 'rule_default', + }, + ], + timelineId: 'some-timeline-id', + timelineTitle: 'Some timeline title', + }), + { + id: 'installed-rule-so-id', + actions, + } + ); - await upgradePrebuiltRules(rulesClient, [{ ...prepackagedRule, actions }]); + beforeEach(() => { + rulesClient.find.mockResolvedValue( + getFindResultWithMultiHits({ + data: [installedRule], + }) + ); + }); - expect(patchRules).toHaveBeenCalledWith( - expect.objectContaining({ - nextParams: expect.objectContaining({ - actions: undefined, - }), - }) - ); + it('deletes rule before creation', async () => { + let lastCalled!: string; - expect(patchRules).toHaveBeenCalledWith( - expect.objectContaining({ - nextParams: expect.objectContaining({ - enabled: undefined, - }), - }) - ); + (deleteRules as jest.Mock).mockImplementation(() => (lastCalled = 'deleteRules')); + (createRules as jest.Mock).mockImplementation(() => (lastCalled = 'createRules')); + + await upgradePrebuiltRules(rulesClient, [prepackagedRule]); + + expect(deleteRules).toHaveBeenCalledTimes(1); + expect(createRules).toHaveBeenCalledTimes(1); + expect(lastCalled).toBe('createRules'); + }); + + it('recreates a rule with incoming version data', async () => { + await upgradePrebuiltRules(rulesClient, [prepackagedRule]); + + expect(createRules).toHaveBeenCalledWith( + expect.objectContaining({ + immutable: true, + params: expect.objectContaining(prepackagedRule), + }) + ); + }); + + it('restores saved object id', async () => { + await upgradePrebuiltRules(rulesClient, [prepackagedRule]); + + expect(createRules).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'installed-rule-so-id', + }) + ); + }); + + it('restores enabled state', async () => { + await upgradePrebuiltRules(rulesClient, [prepackagedRule]); + + expect(createRules).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ enabled: installedRule.enabled }), + }) + ); + }); + + it('restores actions', async () => { + await upgradePrebuiltRules(rulesClient, [prepackagedRule]); + + expect(createRules).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ + actions: actions.map((a) => ({ + ...omit(a, 'actionTypeId'), + action_type_id: a.actionTypeId, + })), + }), + }) + ); + }); + + it('restores exceptions list', async () => { + await upgradePrebuiltRules(rulesClient, [prepackagedRule]); + + expect(createRules).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ exceptions_list: installedRule.params.exceptionsList }), + }) + ); + }); + + it('restores timeline reference', async () => { + await upgradePrebuiltRules(rulesClient, [prepackagedRule]); + + expect(createRules).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ + timeline_id: installedRule.params.timelineId, + timeline_title: installedRule.params.timelineTitle, + }), + }) + ); + }); }); it('should update threat match rules', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/upgrade_prebuilt_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/upgrade_prebuilt_rules.ts index a06b5dc17825f..98afa86114e4f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/upgrade_prebuilt_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/rule_objects/upgrade_prebuilt_rules.ts @@ -72,36 +72,39 @@ const upgradeRule = async ( return createRules({ rulesClient, immutable: true, + id: existingRule.id, params: { ...rule, // Force the prepackaged rule to use the enabled state from the existing rule, // regardless of what the prepackaged rule says enabled: existingRule.enabled, + exceptions_list: existingRule.params.exceptionsList, actions: existingRule.actions.map(transformAlertToRuleAction), + timeline_id: existingRule.params.timelineId, + timeline_title: existingRule.params.timelineTitle, }, }); - } else { - await patchRules({ - rulesClient, - existingRule, - nextParams: { - ...rule, - // Force enabled to use the enabled state from the existing rule by passing in undefined to patchRules - enabled: undefined, - actions: undefined, - }, - }); + } - const updatedRule = await readRules({ - rulesClient, - ruleId: rule.rule_id, - id: undefined, - }); + await patchRules({ + rulesClient, + existingRule, + nextParams: { + ...rule, + // Force enabled to use the enabled state from the existing rule by passing in undefined to patchRules + enabled: undefined, + }, + }); - if (!updatedRule) { - throw new PrepackagedRulesError(`Rule ${rule.rule_id} not found after upgrade`, 500); - } + const updatedRule = await readRules({ + rulesClient, + ruleId: rule.rule_id, + id: undefined, + }); - return updatedRule; + if (!updatedRule) { + throw new PrepackagedRulesError(`Rule ${rule.rule_id} not found after upgrade`, 500); } + + return updatedRule; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.mock.ts index 8329204637c3b..c73203c2871ab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.mock.ts @@ -7,17 +7,19 @@ import type { PrebuiltRuleAsset } from './prebuilt_rule_asset'; -export const getPrebuiltRuleMock = (): PrebuiltRuleAsset => ({ - description: 'some description', - name: 'Query with a rule id', - query: 'user.name: root or user.name: admin', - severity: 'high', - type: 'query', - risk_score: 55, - language: 'kuery', - rule_id: 'rule-1', - version: 1, -}); +export const getPrebuiltRuleMock = (rewrites?: Partial): PrebuiltRuleAsset => + ({ + description: 'some description', + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'query', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + version: 1, + ...rewrites, + } as PrebuiltRuleAsset); export const getPrebuiltRuleWithExceptionsMock = (): PrebuiltRuleAsset => ({ description: 'A rule with an exception list', diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 5b8ec7edd36ab..93f49275491ee 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -568,7 +568,6 @@ export class Plugin implements ISecuritySolutionPlugin { ); turnOffAgentPolicyFeatures( - core.elasticsearch.client.asInternalUser, endpointFleetServicesFactory.asInternalUser(), appFeaturesService, logger diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f985a5e55e58c..9f2a66925c226 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -34322,6 +34322,7 @@ "xpack.securitySolution.endpointConsoleCommands.invalidExecuteTimeout": "L'argument doit être une chaîne avec une valeur entière positive suivie d'une unité de temps (h pour les heures, m pour les minutes, s pour les secondes). Exemple : 37m.", "xpack.securitySolution.endpointConsoleCommands.invalidPidMessage": "L'argument doit être un nombre positif représentant le PID d'un processus.", "xpack.securitySolution.endpointConsoleCommands.isolate.about": "Isoler l'hôte", + "xpack.securitySolution.endpointConsoleCommands.isolate.privileges": "Privilèges insuffisants pour isoler les hôtes. Contactez votre administrateur Kibana si vous pensez que vous devriez bénéficier de cette permission.", "xpack.securitySolution.endpointConsoleCommands.killProcess.about": "Arrêter un processus", "xpack.securitySolution.endpointConsoleCommands.pid.arg.comment": "Un PID représentant le processus à arrêter", "xpack.securitySolution.endpointConsoleCommands.processes.about": "Afficher tous les processus en cours d'exécution", @@ -35482,9 +35483,6 @@ "xpack.securitySolution.responseActions.endpoint.commentDescription": "Laissez une note qui explique ou décrit une action. Vous pouvez voir votre commentaire dans le log de l'historique d'actions de réponse.", "xpack.securitySolution.responseActions.endpoint.commentLabel": "Commentaire (facultatif)", "xpack.securitySolution.responseActions.endpoint.commentLearnMore": "En savoir plus", - "xpack.securitySolution.responseActions.endpoint.isolate": "Isoler", - "xpack.securitySolution.responseActions.endpoint.isolateDescription": "Mettez un hôte du réseau en quarantaine pour empêcher la propagation des menaces et limiter les dégâts potentiels", - "xpack.securitySolution.responseActions.endpoint.isolateTooltip": "Privilèges insuffisants pour isoler les hôtes. Contactez votre administrateur Kibana si vous pensez que vous devriez bénéficier de cette permission.", "xpack.securitySolution.responseActions.endpoint.validations.commandIsRequiredErrorMessage": "Action est un champ requis.", "xpack.securitySolution.responseActionsHistory.empty.content": "Aucune action de réponse effectuée", "xpack.securitySolution.responseActionsHistory.empty.link": "En savoir plus sur les actions de réponse", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 727b85d353289..248e4fddb7f3c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -34322,6 +34322,7 @@ "xpack.securitySolution.endpointConsoleCommands.invalidExecuteTimeout": "引数は、正の整数の文字列の後に時間単位(h:時間、m:分、s:秒)を付けた値でなければなりません。例:37m", "xpack.securitySolution.endpointConsoleCommands.invalidPidMessage": "引数は、プロセスのPIDを表す正の数値でなければなりません", "xpack.securitySolution.endpointConsoleCommands.isolate.about": "ホストの分離", + "xpack.securitySolution.endpointConsoleCommands.isolate.privileges": "ホストを分離するための権限が不足しています。この権限が必要だと思われる場合は、Kibana管理者にお問い合わせください。", "xpack.securitySolution.endpointConsoleCommands.killProcess.about": "プロセスを終了", "xpack.securitySolution.endpointConsoleCommands.pid.arg.comment": "終了するプロセスを表すPID", "xpack.securitySolution.endpointConsoleCommands.processes.about": "すべての実行中のプロセスを表示", @@ -35482,9 +35483,6 @@ "xpack.securitySolution.responseActions.endpoint.commentDescription": "アクションについての説明やメモを残してください。自分のコメントは、対応アクションの履歴ログで確認できます。", "xpack.securitySolution.responseActions.endpoint.commentLabel": "コメント(任意)", "xpack.securitySolution.responseActions.endpoint.commentLearnMore": "詳細", - "xpack.securitySolution.responseActions.endpoint.isolate": "分離", - "xpack.securitySolution.responseActions.endpoint.isolateDescription": "ホストをネットワークから隔離して、脅威のさらなる拡散を防ぎ、潜在的な被害を抑える", - "xpack.securitySolution.responseActions.endpoint.isolateTooltip": "ホストを分離するための権限が不足しています。この権限が必要だと思われる場合は、Kibana管理者にお問い合わせください。", "xpack.securitySolution.responseActions.endpoint.validations.commandIsRequiredErrorMessage": "アクションは必須フィールドです", "xpack.securitySolution.responseActionsHistory.empty.content": "対応アクションログは実行されません", "xpack.securitySolution.responseActionsHistory.empty.link": "対応アクションの詳細を読む", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 827adc04e0444..6c23e8d2f26a3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -34304,6 +34304,7 @@ "xpack.securitySolution.endpointConsoleCommands.invalidExecuteTimeout": "参数必须为含有正整数值的字符串,后接时间单位(h 表示小时,m 表示分钟,s 表示秒)。例如:37m。", "xpack.securitySolution.endpointConsoleCommands.invalidPidMessage": "参数必须为表示进程 PID 的正整数", "xpack.securitySolution.endpointConsoleCommands.isolate.about": "隔离主机", + "xpack.securitySolution.endpointConsoleCommands.isolate.privileges": "权限不足,无法隔离主机。如果认为您应具有此权限,请与 Kibana 管理员联系。", "xpack.securitySolution.endpointConsoleCommands.killProcess.about": "结束/终止进程", "xpack.securitySolution.endpointConsoleCommands.pid.arg.comment": "表示要结束的进程的 PID", "xpack.securitySolution.endpointConsoleCommands.processes.about": "显示所有正在运行的进程", @@ -35464,9 +35465,6 @@ "xpack.securitySolution.responseActions.endpoint.commentDescription": "留下解释或描述该操作的备注。您可以在响应操作历史记录日志中查看注释。", "xpack.securitySolution.responseActions.endpoint.commentLabel": "注释(可选)", "xpack.securitySolution.responseActions.endpoint.commentLearnMore": "了解详情", - "xpack.securitySolution.responseActions.endpoint.isolate": "隔离", - "xpack.securitySolution.responseActions.endpoint.isolateDescription": "将主机与网络隔离,防止威胁进一步扩散并限制潜在损害", - "xpack.securitySolution.responseActions.endpoint.isolateTooltip": "权限不足,无法隔离主机。如果认为您应具有此权限,请与 Kibana 管理员联系。", "xpack.securitySolution.responseActions.endpoint.validations.commandIsRequiredErrorMessage": "“操作”为必填字段。", "xpack.securitySolution.responseActionsHistory.empty.content": "未执行响应操作", "xpack.securitySolution.responseActionsHistory.empty.link": "阅读有关响应操作的更多内容", diff --git a/x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts index a18252b47bdbe..b6653078e18c1 100644 --- a/x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts @@ -527,7 +527,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await transform.sourceSelection.selectSource(testData.source); }); - it('navigates through the wizard and sets all needed fields', async () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/176697 + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/176698 + it.skip('navigates through the wizard and sets all needed fields', async () => { await transform.testExecution.logTestStep('displays the define step'); await transform.wizard.assertDefineStepActive(); diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts index 01cd6174b01cc..3f5e13d0b6615 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts @@ -1296,20 +1296,12 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await header.waitUntilLoadingHasFinished(); - await retry.waitFor('update toast exist', async () => { - return await testSubjects.exists('toastCloseButton'); - }); - - await testSubjects.click('toastCloseButton'); - - await header.waitUntilLoadingHasFinished(); + expect(await textField.getVisibleText()).equal('this is a text field value edited!!'); await toggle.click(); await header.waitUntilLoadingHasFinished(); - expect(await textField.getVisibleText()).equal('this is a text field value edited!!'); - expect(await toggle.getAttribute('aria-checked')).equal('false'); // validate user action diff --git a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts index 6a9232050c3f0..c105b81263362 100644 --- a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts @@ -8,6 +8,7 @@ import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext, kbnTestConfig, kibanaTestUser } from '@kbn/test'; import { services } from '../../../api_integration/services'; +import { PRECONFIGURED_ACTION_CONNECTORS } from '../shared'; interface CreateTestConfigOptions { license: string; @@ -85,20 +86,7 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s 'alertSuppressionForIndicatorMatchRuleEnabled', ])}`, '--xpack.task_manager.poll_interval=1000', - `--xpack.actions.preconfigured=${JSON.stringify({ - 'my-test-email': { - actionTypeId: '.email', - name: 'TestEmail#xyz', - config: { - from: 'me@test.com', - service: '__json', - }, - secrets: { - user: 'user', - password: 'password', - }, - }, - })}`, + `--xpack.actions.preconfigured=${JSON.stringify(PRECONFIGURED_ACTION_CONNECTORS)}`, ...(ssl ? [ `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, diff --git a/x-pack/test/security_solution_api_integration/config/serverless/config.base.ts b/x-pack/test/security_solution_api_integration/config/serverless/config.base.ts index 374538e593efa..ae3b17ce086c3 100644 --- a/x-pack/test/security_solution_api_integration/config/serverless/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/serverless/config.base.ts @@ -12,6 +12,7 @@ export interface CreateTestConfigOptions { kbnTestServerEnv?: Record; } import { services } from '../../../../test_serverless/api_integration/services'; +import { PRECONFIGURED_ACTION_CONNECTORS } from '../shared'; export function createTestConfig(options: CreateTestConfigOptions) { return async ({ readConfigFile }: FtrConfigProviderContext) => { @@ -28,6 +29,7 @@ export function createTestConfig(options: CreateTestConfigOptions) { serverArgs: [ ...svlSharedConfig.get('kbnTestServer.serverArgs'), '--serverless=security', + `--xpack.actions.preconfigured=${JSON.stringify(PRECONFIGURED_ACTION_CONNECTORS)}`, ...(options.kbnTestServerArgs || []), ], env: { diff --git a/x-pack/test/security_solution_api_integration/config/shared.ts b/x-pack/test/security_solution_api_integration/config/shared.ts new file mode 100644 index 0000000000000..f8c55deef484a --- /dev/null +++ b/x-pack/test/security_solution_api_integration/config/shared.ts @@ -0,0 +1,32 @@ +/* + * 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 { Connector } from '@kbn/actions-plugin/server/application/connector/types'; + +interface PreconfiguredConnector extends Pick { + secrets: { + user: string; + password: string; + }; +} + +export const PRECONFIGURED_EMAIL_ACTION_CONNECTOR_ID = 'my-test-email'; + +export const PRECONFIGURED_ACTION_CONNECTORS: Record = { + [PRECONFIGURED_EMAIL_ACTION_CONNECTOR_ID]: { + actionTypeId: '.email', + name: 'TestEmail#xyz', + config: { + from: 'me@test.com', + service: '__json', + }, + secrets: { + user: 'user', + password: 'password', + }, + }, +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/index.ts index 16c37cd09eb5b..5625d00b5293d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/index.ts @@ -11,7 +11,10 @@ export default ({ loadTestFile }: FtrProviderContext): void => { describe('Rules Management - Prebuilt Rules - Prebuilt Rules Management', function () { loadTestFile(require.resolve('./get_prebuilt_rules_status')); loadTestFile(require.resolve('./get_prebuilt_timelines_status')); - loadTestFile(require.resolve('./install_and_upgrade_prebuilt_rules')); + loadTestFile(require.resolve('./install_prebuilt_rules')); + loadTestFile(require.resolve('./install_prebuilt_rules_with_historical_versions')); + loadTestFile(require.resolve('./upgrade_prebuilt_rules')); + loadTestFile(require.resolve('./upgrade_prebuilt_rules_with_historical_versions')); loadTestFile(require.resolve('./fleet_integration')); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/install_and_upgrade_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/install_and_upgrade_prebuilt_rules.ts deleted file mode 100644 index c3d39c532f9ca..0000000000000 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/install_and_upgrade_prebuilt_rules.ts +++ /dev/null @@ -1,450 +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 expect from 'expect'; -import { FtrProviderContext } from '../../../../../../ftr_provider_context'; -import { - deleteAllRules, - deleteAllTimelines, - deleteAllPrebuiltRuleAssets, - createRuleAssetSavedObject, - createPrebuiltRuleAssetSavedObjects, - installPrebuiltRulesAndTimelines, - deleteRule, - getPrebuiltRulesAndTimelinesStatus, - createHistoricalPrebuiltRuleAssetSavedObjects, - getPrebuiltRulesStatus, - installPrebuiltRules, - getInstalledRules, - upgradePrebuiltRules, -} from '../../../../utils'; - -export default ({ getService }: FtrProviderContext): void => { - const es = getService('es'); - const supertest = getService('supertest'); - const log = getService('log'); - - describe('@ess @serverless @skipInQA install and upgrade prebuilt rules with mock rule assets', () => { - beforeEach(async () => { - await deleteAllRules(supertest, log); - await deleteAllTimelines(es, log); - await deleteAllPrebuiltRuleAssets(es, log); - }); - - describe(`rule package without historical versions`, () => { - const getRuleAssetSavedObjects = () => [ - createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }), - createRuleAssetSavedObject({ rule_id: 'rule-2', version: 2 }), - createRuleAssetSavedObject({ rule_id: 'rule-3', version: 3 }), - createRuleAssetSavedObject({ rule_id: 'rule-4', version: 4 }), - ]; - const RULES_COUNT = 4; - - describe('using legacy endpoint', () => { - it('should install prebuilt rules', async () => { - await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - const body = await installPrebuiltRulesAndTimelines(es, supertest); - - expect(body.rules_installed).toBe(RULES_COUNT); - expect(body.rules_updated).toBe(0); - }); - - it('should install correct prebuilt rule versions', async () => { - await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(es, supertest); - - // Get installed rules - const rulesResponse = await getInstalledRules(supertest); - - // Check that all prebuilt rules were actually installed and their versions match the latest - expect(rulesResponse.total).toBe(RULES_COUNT); - expect(rulesResponse.data).toEqual( - expect.arrayContaining([ - expect.objectContaining({ rule_id: 'rule-1', version: 1 }), - expect.objectContaining({ rule_id: 'rule-2', version: 2 }), - expect.objectContaining({ rule_id: 'rule-3', version: 3 }), - expect.objectContaining({ rule_id: 'rule-4', version: 4 }), - ]) - ); - }); - - it('should install missing prebuilt rules', async () => { - // Install all prebuilt detection rules - await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(es, supertest); - - // Delete one of the installed rules - await deleteRule(supertest, 'rule-1'); - - // Check that one prebuilt rule is missing - const statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); - expect(statusResponse.rules_not_installed).toBe(1); - - // Call the install prebuilt rules again and check that the missing rule was installed - const response = await installPrebuiltRulesAndTimelines(es, supertest); - expect(response.rules_installed).toBe(1); - expect(response.rules_updated).toBe(0); - }); - - it('should update outdated prebuilt rules', async () => { - // Install all prebuilt detection rules - const ruleAssetSavedObjects = getRuleAssetSavedObjects(); - await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); - await installPrebuiltRulesAndTimelines(es, supertest); - - // Clear previous rule assets - await deleteAllPrebuiltRuleAssets(es, log); - // Increment the version of one of the installed rules and create the new rule assets - ruleAssetSavedObjects[0]['security-rule'].version += 1; - await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); - - // Check that one prebuilt rule status shows that one rule is outdated - const statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); - expect(statusResponse.rules_not_updated).toBe(1); - - // Call the install prebuilt rules again and check that the outdated rule was updated - const response = await installPrebuiltRulesAndTimelines(es, supertest); - expect(response.rules_installed).toBe(0); - expect(response.rules_updated).toBe(1); - }); - - it('should not install prebuilt rules if they are up to date', async () => { - // Install all prebuilt detection rules - await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(es, supertest); - - // Check that all prebuilt rules were installed - const statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); - expect(statusResponse.rules_not_installed).toBe(0); - expect(statusResponse.rules_not_updated).toBe(0); - - // Call the install prebuilt rules again and check that no rules were installed - const response = await installPrebuiltRulesAndTimelines(es, supertest); - expect(response.rules_installed).toBe(0); - expect(response.rules_updated).toBe(0); - }); - }); - - describe('using current endpoint', () => { - it('should install prebuilt rules', async () => { - await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - const body = await installPrebuiltRules(es, supertest); - - expect(body.summary.succeeded).toBe(RULES_COUNT); - expect(body.summary.failed).toBe(0); - expect(body.summary.skipped).toBe(0); - }); - - it('should install correct prebuilt rule versions', async () => { - await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - const body = await installPrebuiltRules(es, supertest); - - // Check that all prebuilt rules were actually installed and their versions match the latest - expect(body.results.created).toEqual( - expect.arrayContaining([ - expect.objectContaining({ rule_id: 'rule-1', version: 1 }), - expect.objectContaining({ rule_id: 'rule-2', version: 2 }), - expect.objectContaining({ rule_id: 'rule-3', version: 3 }), - expect.objectContaining({ rule_id: 'rule-4', version: 4 }), - ]) - ); - }); - - it('should install missing prebuilt rules', async () => { - // Install all prebuilt detection rules - await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(es, supertest); - - // Delete one of the installed rules - await deleteRule(supertest, 'rule-1'); - - // Check that one prebuilt rule is missing - const statusResponse = await getPrebuiltRulesStatus(es, supertest); - expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(1); - - // Call the install prebuilt rules again and check that the missing rule was installed - const response = await installPrebuiltRules(es, supertest); - expect(response.summary.succeeded).toBe(1); - }); - - it('should update outdated prebuilt rules', async () => { - // Install all prebuilt detection rules - const ruleAssetSavedObjects = getRuleAssetSavedObjects(); - await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); - await installPrebuiltRules(es, supertest); - - // Clear previous rule assets - await deleteAllPrebuiltRuleAssets(es, log); - // Increment the version of one of the installed rules and create the new rule assets - ruleAssetSavedObjects[0]['security-rule'].version += 1; - await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); - - // Check that one prebuilt rule status shows that one rule is outdated - const statusResponse = await getPrebuiltRulesStatus(es, supertest); - expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(0); - expect(statusResponse.stats.num_prebuilt_rules_to_upgrade).toBe(1); - - // Call the install prebuilt rules again and check that the outdated rule was updated - const response = await upgradePrebuiltRules(es, supertest); - expect(response.summary.succeeded).toBe(1); - expect(response.summary.skipped).toBe(0); - }); - - it('should not install prebuilt rules if they are up to date', async () => { - // Install all prebuilt detection rules - await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(es, supertest); - - // Check that all prebuilt rules were installed - const statusResponse = await getPrebuiltRulesStatus(es, supertest); - expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(0); - expect(statusResponse.stats.num_prebuilt_rules_to_upgrade).toBe(0); - - // Call the install prebuilt rules again and check that no rules were installed - const installResponse = await installPrebuiltRules(es, supertest); - expect(installResponse.summary.succeeded).toBe(0); - expect(installResponse.summary.skipped).toBe(0); - - // Call the upgrade prebuilt rules endpoint and check that no rules were updated - const upgradeResponse = await upgradePrebuiltRules(es, supertest); - expect(upgradeResponse.summary.succeeded).toBe(0); - expect(upgradeResponse.summary.skipped).toBe(0); - }); - }); - }); - - describe(`rule package with historical versions`, () => { - const getRuleAssetSavedObjects = () => [ - createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }), - createRuleAssetSavedObject({ rule_id: 'rule-1', version: 2 }), - createRuleAssetSavedObject({ rule_id: 'rule-2', version: 1 }), - createRuleAssetSavedObject({ rule_id: 'rule-2', version: 2 }), - createRuleAssetSavedObject({ rule_id: 'rule-2', version: 3 }), - ]; - const RULES_COUNT = 2; - - describe('using legacy endpoint', () => { - it('should install prebuilt rules', async () => { - await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - const body = await installPrebuiltRulesAndTimelines(es, supertest); - - expect(body.rules_installed).toBe(RULES_COUNT); - expect(body.rules_updated).toBe(0); - }); - - it('should install correct prebuilt rule versions', async () => { - await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(es, supertest); - - // Get installed rules - const rulesResponse = await getInstalledRules(supertest); - - // Check that all prebuilt rules were actually installed and their versions match the latest - expect(rulesResponse.total).toBe(RULES_COUNT); - expect(rulesResponse.data).toEqual( - expect.arrayContaining([ - expect.objectContaining({ rule_id: 'rule-1', version: 2 }), - expect.objectContaining({ rule_id: 'rule-2', version: 3 }), - ]) - ); - }); - - it('should not install prebuilt rules if they are up to date', async () => { - // Install all prebuilt detection rules - await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(es, supertest); - - // Check that all prebuilt rules were installed - const statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); - expect(statusResponse.rules_not_installed).toBe(0); - - // Call the install prebuilt rules again and check that no rules were installed - const response = await installPrebuiltRulesAndTimelines(es, supertest); - expect(response.rules_installed).toBe(0); - expect(response.rules_updated).toBe(0); - }); - - it('should install missing prebuilt rules', async () => { - // Install all prebuilt detection rules - await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(es, supertest); - - // Delete one of the installed rules - await deleteRule(supertest, 'rule-1'); - - // Check that one prebuilt rule is missing - const statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); - expect(statusResponse.rules_not_installed).toBe(1); - - // Call the install prebuilt rules endpoint again and check that the missing rule was installed - const response = await installPrebuiltRulesAndTimelines(es, supertest); - expect(response.rules_installed).toBe(1); - expect(response.rules_updated).toBe(0); - }); - - it('should update outdated prebuilt rules when previous historical versions available', async () => { - // Install all prebuilt detection rules - await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(es, supertest); - - // Add a new version of one of the installed rules - await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ - createRuleAssetSavedObject({ rule_id: 'rule-1', version: 3 }), - ]); - - // Check that one prebuilt rule status shows that one rule is outdated - const statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); - expect(statusResponse.rules_not_updated).toBe(1); - - // Call the install prebuilt rules again and check that the outdated rule was updated - const response = await installPrebuiltRulesAndTimelines(es, supertest); - expect(response.rules_installed).toBe(0); - expect(response.rules_updated).toBe(1); - - const _statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); - expect(_statusResponse.rules_not_installed).toBe(0); - expect(_statusResponse.rules_not_updated).toBe(0); - }); - - it('should update outdated prebuilt rules when previous historical versions unavailable', async () => { - // Install all prebuilt detection rules - await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRulesAndTimelines(es, supertest); - - // Clear previous rule assets - await deleteAllPrebuiltRuleAssets(es, log); - - // Add a new rule version - await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ - createRuleAssetSavedObject({ rule_id: 'rule-1', version: 3 }), - ]); - - // Check that one prebuilt rule status shows that one rule is outdated - const statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); - expect(statusResponse.rules_not_updated).toBe(1); - expect(statusResponse.rules_not_installed).toBe(0); - - // Call the install prebuilt rules again and check that the outdated rule was updated - const response = await installPrebuiltRulesAndTimelines(es, supertest); - expect(response.rules_installed).toBe(0); - expect(response.rules_updated).toBe(1); - - const _statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); - expect(_statusResponse.rules_not_updated).toBe(0); - expect(_statusResponse.rules_not_installed).toBe(0); - }); - }); - - describe('using current endpoint', () => { - it('should install prebuilt rules', async () => { - await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - const body = await installPrebuiltRules(es, supertest); - - expect(body.summary.succeeded).toBe(RULES_COUNT); - }); - - it('should install correct prebuilt rule versions', async () => { - await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - const response = await installPrebuiltRules(es, supertest); - - // Check that all prebuilt rules were actually installed and their versions match the latest - expect(response.summary.succeeded).toBe(RULES_COUNT); - expect(response.results.created).toEqual( - expect.arrayContaining([ - expect.objectContaining({ rule_id: 'rule-1', version: 2 }), - expect.objectContaining({ rule_id: 'rule-2', version: 3 }), - ]) - ); - }); - - it('should not install prebuilt rules if they are up to date', async () => { - // Install all prebuilt detection rules - await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(es, supertest); - - // Check that all prebuilt rules were installed - const statusResponse = await getPrebuiltRulesStatus(es, supertest); - expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(0); - - // Call the install prebuilt rules again and check that no rules were installed - const response = await installPrebuiltRules(es, supertest); - expect(response.summary.succeeded).toBe(0); - expect(response.summary.total).toBe(0); - }); - - it('should install missing prebuilt rules', async () => { - // Install all prebuilt detection rules - await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(es, supertest); - - // Delete one of the installed rules - await deleteRule(supertest, 'rule-1'); - - // Check that one prebuilt rule is missing - const statusResponse = await getPrebuiltRulesStatus(es, supertest); - expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(1); - - // Call the install prebuilt rules endpoint again and check that the missing rule was installed - const response = await installPrebuiltRules(es, supertest); - expect(response.summary.succeeded).toBe(1); - expect(response.summary.total).toBe(1); - }); - - it('should update outdated prebuilt rules when previous historical versions available', async () => { - // Install all prebuilt detection rules - await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(es, supertest); - - // Add a new version of one of the installed rules - await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ - createRuleAssetSavedObject({ rule_id: 'rule-1', version: 3 }), - ]); - - // Check that the prebuilt rule status shows that one rule is outdated - const statusResponse = await getPrebuiltRulesStatus(es, supertest); - expect(statusResponse.stats.num_prebuilt_rules_to_upgrade).toBe(1); - - // Call the upgrade prebuilt rules endpoint and check that the outdated rule was updated - const response = await upgradePrebuiltRules(es, supertest); - expect(response.summary.succeeded).toBe(1); - expect(response.summary.total).toBe(1); - - const status = await getPrebuiltRulesStatus(es, supertest); - expect(status.stats.num_prebuilt_rules_to_install).toBe(0); - expect(status.stats.num_prebuilt_rules_to_upgrade).toBe(0); - }); - - it('should update outdated prebuilt rules when previous historical versions unavailable', async () => { - // Install all prebuilt detection rules - await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); - await installPrebuiltRules(es, supertest); - - // Clear previous rule assets - await deleteAllPrebuiltRuleAssets(es, log); - - // Add a new rule version - await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ - createRuleAssetSavedObject({ rule_id: 'rule-1', version: 3 }), - ]); - - // Check that the prebuilt rule status shows that one rule is outdated - const statusResponse = await getPrebuiltRulesStatus(es, supertest); - expect(statusResponse.stats.num_prebuilt_rules_to_upgrade).toBe(1); - expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(0); - - // Call the upgrade prebuilt rules endpoint and check that the outdated rule was updated - const response = await upgradePrebuiltRules(es, supertest); - expect(response.summary.succeeded).toBe(1); - expect(response.summary.total).toBe(1); - - const status = await getPrebuiltRulesStatus(es, supertest); - expect(status.stats.num_prebuilt_rules_to_install).toBe(0); - expect(status.stats.num_prebuilt_rules_to_upgrade).toBe(0); - }); - }); - }); - }); -}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/install_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/install_prebuilt_rules.ts new file mode 100644 index 0000000000000..e9b8bbed84d1e --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/install_prebuilt_rules.ts @@ -0,0 +1,133 @@ +/* + * 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 expect from 'expect'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + deleteAllRules, + deleteAllTimelines, + deleteAllPrebuiltRuleAssets, + createRuleAssetSavedObject, + createPrebuiltRuleAssetSavedObjects, + installPrebuiltRulesAndTimelines, + deleteRule, + getPrebuiltRulesAndTimelinesStatus, + getPrebuiltRulesStatus, + installPrebuiltRules, + getInstalledRules, +} from '../../../../utils'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + + describe('@ess @serverless @skipInQA install prebuilt rules from package without historical versions with mock rule assets', () => { + const getRuleAssetSavedObjects = () => [ + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }), + createRuleAssetSavedObject({ rule_id: 'rule-2', version: 2 }), + createRuleAssetSavedObject({ rule_id: 'rule-3', version: 3 }), + createRuleAssetSavedObject({ rule_id: 'rule-4', version: 4 }), + ]; + const RULES_COUNT = getRuleAssetSavedObjects().length; + + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllTimelines(es, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + describe('using current endpoint', () => { + it('should install prebuilt rules', async () => { + await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + const body = await installPrebuiltRules(es, supertest); + + expect(body.summary.succeeded).toBe(RULES_COUNT); + expect(body.summary.failed).toBe(0); + expect(body.summary.skipped).toBe(0); + }); + + it('should install correct prebuilt rule versions', async () => { + await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + const body = await installPrebuiltRules(es, supertest); + + // Check that all prebuilt rules were actually installed and their versions match the latest + expect(body.results.created).toEqual( + expect.arrayContaining([ + expect.objectContaining({ rule_id: 'rule-1', version: 1 }), + expect.objectContaining({ rule_id: 'rule-2', version: 2 }), + expect.objectContaining({ rule_id: 'rule-3', version: 3 }), + expect.objectContaining({ rule_id: 'rule-4', version: 4 }), + ]) + ); + }); + + it('should install missing prebuilt rules', async () => { + // Install all prebuilt detection rules + await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Delete one of the installed rules + await deleteRule(supertest, 'rule-1'); + + // Check that one prebuilt rule is missing + const statusResponse = await getPrebuiltRulesStatus(es, supertest); + expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(1); + + // Call the install prebuilt rules again and check that the missing rule was installed + const response = await installPrebuiltRules(es, supertest); + expect(response.summary.succeeded).toBe(1); + }); + }); + + describe('using legacy endpoint', () => { + it('should install prebuilt rules', async () => { + await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + const body = await installPrebuiltRulesAndTimelines(es, supertest); + + expect(body.rules_installed).toBe(RULES_COUNT); + expect(body.rules_updated).toBe(0); + }); + + it('should install correct prebuilt rule versions', async () => { + await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRulesAndTimelines(es, supertest); + + // Get installed rules + const rulesResponse = await getInstalledRules(supertest); + + // Check that all prebuilt rules were actually installed and their versions match the latest + expect(rulesResponse.total).toBe(RULES_COUNT); + expect(rulesResponse.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ rule_id: 'rule-1', version: 1 }), + expect.objectContaining({ rule_id: 'rule-2', version: 2 }), + expect.objectContaining({ rule_id: 'rule-3', version: 3 }), + expect.objectContaining({ rule_id: 'rule-4', version: 4 }), + ]) + ); + }); + + it('should install missing prebuilt rules', async () => { + // Install all prebuilt detection rules + await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRulesAndTimelines(es, supertest); + + // Delete one of the installed rules + await deleteRule(supertest, 'rule-1'); + + // Check that one prebuilt rule is missing + const statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); + expect(statusResponse.rules_not_installed).toBe(1); + + // Call the install prebuilt rules again and check that the missing rule was installed + const response = await installPrebuiltRulesAndTimelines(es, supertest); + expect(response.rules_installed).toBe(1); + expect(response.rules_updated).toBe(0); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/install_prebuilt_rules_with_historical_versions.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/install_prebuilt_rules_with_historical_versions.ts new file mode 100644 index 0000000000000..6120caa8eda22 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/install_prebuilt_rules_with_historical_versions.ts @@ -0,0 +1,160 @@ +/* + * 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 expect from 'expect'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + deleteAllRules, + deleteAllTimelines, + deleteAllPrebuiltRuleAssets, + createRuleAssetSavedObject, + installPrebuiltRulesAndTimelines, + deleteRule, + getPrebuiltRulesAndTimelinesStatus, + createHistoricalPrebuiltRuleAssetSavedObjects, + getPrebuiltRulesStatus, + installPrebuiltRules, + getInstalledRules, +} from '../../../../utils'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + + describe('@ess @serverless @skipInQA install prebuilt rules from package with historical versions with mock rule assets', () => { + const getRuleAssetSavedObjects = () => [ + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }), + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 2 }), + createRuleAssetSavedObject({ rule_id: 'rule-2', version: 1 }), + createRuleAssetSavedObject({ rule_id: 'rule-2', version: 2 }), + createRuleAssetSavedObject({ rule_id: 'rule-2', version: 3 }), + ]; + const RULES_COUNT = 2; + + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllTimelines(es, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + describe('using legacy endpoint', () => { + it('should install prebuilt rules', async () => { + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + const body = await installPrebuiltRulesAndTimelines(es, supertest); + + expect(body.rules_installed).toBe(RULES_COUNT); + expect(body.rules_updated).toBe(0); + }); + + it('should install correct prebuilt rule versions', async () => { + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRulesAndTimelines(es, supertest); + + // Get installed rules + const rulesResponse = await getInstalledRules(supertest); + + // Check that all prebuilt rules were actually installed and their versions match the latest + expect(rulesResponse.total).toBe(RULES_COUNT); + expect(rulesResponse.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ rule_id: 'rule-1', version: 2 }), + expect.objectContaining({ rule_id: 'rule-2', version: 3 }), + ]) + ); + }); + + it('should not install prebuilt rules if they are up to date', async () => { + // Install all prebuilt detection rules + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRulesAndTimelines(es, supertest); + + // Check that all prebuilt rules were installed + const statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); + expect(statusResponse.rules_not_installed).toBe(0); + + // Call the install prebuilt rules again and check that no rules were installed + const response = await installPrebuiltRulesAndTimelines(es, supertest); + expect(response.rules_installed).toBe(0); + expect(response.rules_updated).toBe(0); + }); + + it('should install missing prebuilt rules', async () => { + // Install all prebuilt detection rules + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRulesAndTimelines(es, supertest); + + // Delete one of the installed rules + await deleteRule(supertest, 'rule-1'); + + // Check that one prebuilt rule is missing + const statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); + expect(statusResponse.rules_not_installed).toBe(1); + + // Call the install prebuilt rules endpoint again and check that the missing rule was installed + const response = await installPrebuiltRulesAndTimelines(es, supertest); + expect(response.rules_installed).toBe(1); + expect(response.rules_updated).toBe(0); + }); + }); + + describe('using current endpoint', () => { + it('should install prebuilt rules', async () => { + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + const body = await installPrebuiltRules(es, supertest); + + expect(body.summary.succeeded).toBe(RULES_COUNT); + }); + + it('should install correct prebuilt rule versions', async () => { + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + const response = await installPrebuiltRules(es, supertest); + + // Check that all prebuilt rules were actually installed and their versions match the latest + expect(response.summary.succeeded).toBe(RULES_COUNT); + expect(response.results.created).toEqual( + expect.arrayContaining([ + expect.objectContaining({ rule_id: 'rule-1', version: 2 }), + expect.objectContaining({ rule_id: 'rule-2', version: 3 }), + ]) + ); + }); + + it('should not install prebuilt rules if they are up to date', async () => { + // Install all prebuilt detection rules + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Check that all prebuilt rules were installed + const statusResponse = await getPrebuiltRulesStatus(es, supertest); + expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(0); + + // Call the install prebuilt rules again and check that no rules were installed + const response = await installPrebuiltRules(es, supertest); + expect(response.summary.succeeded).toBe(0); + expect(response.summary.total).toBe(0); + }); + + it('should install missing prebuilt rules', async () => { + // Install all prebuilt detection rules + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Delete one of the installed rules + await deleteRule(supertest, 'rule-1'); + + // Check that one prebuilt rule is missing + const statusResponse = await getPrebuiltRulesStatus(es, supertest); + expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(1); + + // Call the install prebuilt rules endpoint again and check that the missing rule was installed + const response = await installPrebuiltRules(es, supertest); + expect(response.summary.succeeded).toBe(1); + expect(response.summary.total).toBe(1); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_prebuilt_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_prebuilt_rules.ts new file mode 100644 index 0000000000000..5d1f9662e118a --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_prebuilt_rules.ts @@ -0,0 +1,268 @@ +/* + * 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 expect from 'expect'; +import { PRECONFIGURED_EMAIL_ACTION_CONNECTOR_ID } from '../../../../../../config/shared'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + deleteAllRules, + deleteAllTimelines, + deleteAllPrebuiltRuleAssets, + createRuleAssetSavedObject, + createPrebuiltRuleAssetSavedObjects, + installPrebuiltRulesAndTimelines, + getPrebuiltRulesAndTimelinesStatus, + getPrebuiltRulesStatus, + installPrebuiltRules, + upgradePrebuiltRules, + fetchRule, + patchRule, +} from '../../../../utils'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + + describe('@ess @serverless @skipInQA upgrade prebuilt rules from package without historical versions with mock rule assets', () => { + const getRuleAssetSavedObjects = () => [ + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }), + createRuleAssetSavedObject({ rule_id: 'rule-2', version: 2 }), + createRuleAssetSavedObject({ rule_id: 'rule-3', version: 3 }), + createRuleAssetSavedObject({ rule_id: 'rule-4', version: 4 }), + ]; + + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllTimelines(es, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + describe('using legacy endpoint', () => { + it('should upgrade outdated prebuilt rules', async () => { + // Install all prebuilt detection rules + const ruleAssetSavedObjects = getRuleAssetSavedObjects(); + await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); + await installPrebuiltRulesAndTimelines(es, supertest); + + // Clear previous rule assets + await deleteAllPrebuiltRuleAssets(es, log); + // Increment the version of one of the installed rules and create the new rule assets + ruleAssetSavedObjects[0]['security-rule'].version += 1; + await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); + + // Check that one prebuilt rule status shows that one rule is outdated + const statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); + expect(statusResponse.rules_not_updated).toBe(1); + + // Call the install prebuilt rules again and check that the outdated rule was updated + const response = await installPrebuiltRulesAndTimelines(es, supertest); + expect(response.rules_installed).toBe(0); + expect(response.rules_updated).toBe(1); + }); + + it('should not upgrade prebuilt rules if they are up to date', async () => { + // Install all prebuilt detection rules + await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRulesAndTimelines(es, supertest); + + // Check that all prebuilt rules were installed + const statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); + expect(statusResponse.rules_not_installed).toBe(0); + expect(statusResponse.rules_not_updated).toBe(0); + + // Call the install prebuilt rules again and check that no rules were installed + const response = await installPrebuiltRulesAndTimelines(es, supertest); + expect(response.rules_installed).toBe(0); + expect(response.rules_updated).toBe(0); + }); + }); + + describe('using current endpoint', () => { + it('should upgrade outdated prebuilt rules', async () => { + // Install all prebuilt detection rules + const ruleAssetSavedObjects = getRuleAssetSavedObjects(); + await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); + await installPrebuiltRules(es, supertest); + + // Clear previous rule assets + await deleteAllPrebuiltRuleAssets(es, log); + // Increment the version of one of the installed rules and create the new rule assets + ruleAssetSavedObjects[0]['security-rule'].version += 1; + await createPrebuiltRuleAssetSavedObjects(es, ruleAssetSavedObjects); + + // Check that one prebuilt rule status shows that one rule is outdated + const statusResponse = await getPrebuiltRulesStatus(es, supertest); + expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(0); + expect(statusResponse.stats.num_prebuilt_rules_to_upgrade).toBe(1); + + // Call the install prebuilt rules again and check that the outdated rule was updated + const response = await upgradePrebuiltRules(es, supertest); + expect(response.summary.succeeded).toBe(1); + expect(response.summary.skipped).toBe(0); + }); + + it('should not upgrade prebuilt rules if they are up to date', async () => { + // Install all prebuilt detection rules + await createPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Check that all prebuilt rules were installed + const statusResponse = await getPrebuiltRulesStatus(es, supertest); + expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(0); + expect(statusResponse.stats.num_prebuilt_rules_to_upgrade).toBe(0); + + // Call the install prebuilt rules again and check that no rules were installed + const installResponse = await installPrebuiltRules(es, supertest); + expect(installResponse.summary.succeeded).toBe(0); + expect(installResponse.summary.skipped).toBe(0); + + // Call the upgrade prebuilt rules endpoint and check that no rules were updated + const upgradeResponse = await upgradePrebuiltRules(es, supertest); + expect(upgradeResponse.summary.succeeded).toBe(0); + expect(upgradeResponse.summary.skipped).toBe(0); + }); + + describe('when upgrading a prebuilt rule to a newer version with the same rule type', () => { + it('preserves rule bound data', async () => { + await createPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ + rule_id: 'rule-to-test-1', + enabled: true, + version: 1, + }), + ]); + const firstInstallResponse = await installPrebuiltRules(es, supertest); + const initialRuleSoId = firstInstallResponse.results.created[0].id; + + const actions = [ + // Use a preconfigured action connector to simplify the test and avoid action connector creation + { + id: PRECONFIGURED_EMAIL_ACTION_CONNECTOR_ID, + action_type_id: '.email', + group: 'default', + params: {}, + }, + ]; + const exceptionsList = [ + { + id: 'exception_list_1', + list_id: 'exception_list_1', + namespace_type: 'agnostic', + type: 'rule_default', + } as const, + ]; + + // Add some actions, exceptions list, and timeline reference + await patchRule(supertest, log, { + rule_id: 'rule-to-test-1', + enabled: false, + actions, + exceptions_list: exceptionsList, + timeline_id: 'some-timeline-id', + timeline_title: 'Some timeline title', + }); + + // Clear previous rule assets + await deleteAllPrebuiltRuleAssets(es, log); + // Create a new version with the same rule type asset + await createPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ + rule_id: 'rule-to-test-1', + enabled: true, + version: 2, + }), + ]); + + // Upgrade to a newer version with the same type + await upgradePrebuiltRules(es, supertest); + + expect(await fetchRule(supertest, { ruleId: 'rule-to-test-1' })).toMatchObject({ + id: initialRuleSoId, + // If a user disabled the rule it's expected to stay disabled after upgrade + enabled: false, + actions, + exceptions_list: exceptionsList, + timeline_id: 'some-timeline-id', + timeline_title: 'Some timeline title', + }); + }); + }); + + describe('when upgrading a prebuilt rule to a newer version with a different rule type', () => { + it('preserves rule bound data', async () => { + await createPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ + rule_id: 'rule-to-test-2', + type: 'query', + language: 'kuery', + query: '*:*', + enabled: true, + version: 1, + }), + ]); + const firstInstallResponse = await installPrebuiltRules(es, supertest); + const initialRuleSoId = firstInstallResponse.results.created[0].id; + + const actions = [ + // Use a preconfigured action connector to simplify the test and avoid action connector creation + { + id: PRECONFIGURED_EMAIL_ACTION_CONNECTOR_ID, + action_type_id: '.email', + group: 'default', + params: {}, + }, + ]; + const exceptionsList = [ + { + id: 'exception_list_1', + list_id: 'exception_list_1', + namespace_type: 'agnostic', + type: 'rule_default', + } as const, + ]; + + // Add some actions, exceptions list, and timeline reference + await patchRule(supertest, log, { + rule_id: 'rule-to-test-2', + enabled: false, + actions, + exceptions_list: exceptionsList, + timeline_id: 'some-timeline-id', + timeline_title: 'Some timeline title', + }); + + // Clear previous rule assets + await deleteAllPrebuiltRuleAssets(es, log); + // Create a new version with a different rule type asset + await createPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ + rule_id: 'rule-to-test-2', + type: 'eql', + language: 'eql', + query: 'host where host == "something"', + enabled: true, + version: 2, + }), + ]); + + // Upgrade to a newer version with a different rule type + await upgradePrebuiltRules(es, supertest); + + expect(await fetchRule(supertest, { ruleId: 'rule-to-test-2' })).toMatchObject({ + id: initialRuleSoId, + // If a user disabled the rule it's expected to stay disabled after upgrade + enabled: false, + actions, + exceptions_list: exceptionsList, + timeline_id: 'some-timeline-id', + timeline_title: 'Some timeline title', + }); + }); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_prebuilt_rules_with_historical_versions.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_prebuilt_rules_with_historical_versions.ts new file mode 100644 index 0000000000000..cd6ff46ecabb1 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_prebuilt_rules_with_historical_versions.ts @@ -0,0 +1,152 @@ +/* + * 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 expect from 'expect'; +import { FtrProviderContext } from '../../../../../../ftr_provider_context'; +import { + deleteAllRules, + deleteAllTimelines, + deleteAllPrebuiltRuleAssets, + createRuleAssetSavedObject, + installPrebuiltRulesAndTimelines, + getPrebuiltRulesAndTimelinesStatus, + createHistoricalPrebuiltRuleAssetSavedObjects, + getPrebuiltRulesStatus, + installPrebuiltRules, + upgradePrebuiltRules, +} from '../../../../utils'; + +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + + describe('@ess @serverless @skipInQA upgrade prebuilt rules from package with historical versions with mock rule assets', () => { + beforeEach(async () => { + await deleteAllRules(supertest, log); + await deleteAllTimelines(es, log); + await deleteAllPrebuiltRuleAssets(es, log); + }); + + describe(`rule package with historical versions`, () => { + const getRuleAssetSavedObjects = () => [ + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 1 }), + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 2 }), + createRuleAssetSavedObject({ rule_id: 'rule-2', version: 1 }), + createRuleAssetSavedObject({ rule_id: 'rule-2', version: 2 }), + createRuleAssetSavedObject({ rule_id: 'rule-2', version: 3 }), + ]; + + describe('using legacy endpoint', () => { + it('should upgrade outdated prebuilt rules when previous historical versions available', async () => { + // Install all prebuilt detection rules + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRulesAndTimelines(es, supertest); + + // Add a new version of one of the installed rules + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 3 }), + ]); + + // Check that one prebuilt rule status shows that one rule is outdated + const statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); + expect(statusResponse.rules_not_updated).toBe(1); + + // Call the install prebuilt rules again and check that the outdated rule was updated + const response = await installPrebuiltRulesAndTimelines(es, supertest); + expect(response.rules_installed).toBe(0); + expect(response.rules_updated).toBe(1); + + const _statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); + expect(_statusResponse.rules_not_installed).toBe(0); + expect(_statusResponse.rules_not_updated).toBe(0); + }); + + it('should upgrade outdated prebuilt rules when previous historical versions unavailable', async () => { + // Install all prebuilt detection rules + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRulesAndTimelines(es, supertest); + + // Clear previous rule assets + await deleteAllPrebuiltRuleAssets(es, log); + + // Add a new rule version + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 3 }), + ]); + + // Check that one prebuilt rule status shows that one rule is outdated + const statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); + expect(statusResponse.rules_not_updated).toBe(1); + expect(statusResponse.rules_not_installed).toBe(0); + + // Call the install prebuilt rules again and check that the outdated rule was updated + const response = await installPrebuiltRulesAndTimelines(es, supertest); + expect(response.rules_installed).toBe(0); + expect(response.rules_updated).toBe(1); + + const _statusResponse = await getPrebuiltRulesAndTimelinesStatus(es, supertest); + expect(_statusResponse.rules_not_updated).toBe(0); + expect(_statusResponse.rules_not_installed).toBe(0); + }); + }); + + describe('using current endpoint', () => { + it('should upgrade outdated prebuilt rules when previous historical versions available', async () => { + // Install all prebuilt detection rules + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Add a new version of one of the installed rules + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 3 }), + ]); + + // Check that the prebuilt rule status shows that one rule is outdated + const statusResponse = await getPrebuiltRulesStatus(es, supertest); + expect(statusResponse.stats.num_prebuilt_rules_to_upgrade).toBe(1); + + // Call the upgrade prebuilt rules endpoint and check that the outdated rule was updated + const response = await upgradePrebuiltRules(es, supertest); + expect(response.summary.succeeded).toBe(1); + expect(response.summary.total).toBe(1); + + const status = await getPrebuiltRulesStatus(es, supertest); + expect(status.stats.num_prebuilt_rules_to_install).toBe(0); + expect(status.stats.num_prebuilt_rules_to_upgrade).toBe(0); + }); + + it('should upgrade outdated prebuilt rules when previous historical versions unavailable', async () => { + // Install all prebuilt detection rules + await createHistoricalPrebuiltRuleAssetSavedObjects(es, getRuleAssetSavedObjects()); + await installPrebuiltRules(es, supertest); + + // Clear previous rule assets + await deleteAllPrebuiltRuleAssets(es, log); + + // Add a new rule version + await createHistoricalPrebuiltRuleAssetSavedObjects(es, [ + createRuleAssetSavedObject({ rule_id: 'rule-1', version: 3 }), + ]); + + // Check that the prebuilt rule status shows that one rule is outdated + const statusResponse = await getPrebuiltRulesStatus(es, supertest); + expect(statusResponse.stats.num_prebuilt_rules_to_upgrade).toBe(1); + expect(statusResponse.stats.num_prebuilt_rules_to_install).toBe(0); + + // Call the upgrade prebuilt rules endpoint and check that the outdated rule was updated + const response = await upgradePrebuiltRules(es, supertest); + expect(response.summary.succeeded).toBe(1); + expect(response.summary.total).toBe(1); + + const status = await getPrebuiltRulesStatus(es, supertest); + expect(status.stats.num_prebuilt_rules_to_install).toBe(0); + expect(status.stats.num_prebuilt_rules_to_upgrade).toBe(0); + }); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts index b87e1e0fcc4b1..555e845ec7b4f 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_import_export/trial_license_complete_tier/export_rules.ts @@ -9,6 +9,7 @@ import expect from 'expect'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; import { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { PRECONFIGURED_EMAIL_ACTION_CONNECTOR_ID } from '../../../../../config/shared'; import { binaryToString, createRule, @@ -381,7 +382,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should export rule without the action connector if it is Preconfigured Connector', async () => { const action = { group: 'default', - id: 'my-test-email', + id: PRECONFIGURED_EMAIL_ACTION_CONNECTOR_ID, action_type_id: '.email', params: {}, }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/create_prebuilt_rule_saved_objects.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/create_prebuilt_rule_saved_objects.ts index 0b4bfd9254b15..20a8e6cf17280 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/create_prebuilt_rule_saved_objects.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/prebuilt_rules/create_prebuilt_rule_saved_objects.ts @@ -21,10 +21,7 @@ import { SECURITY_SOLUTION_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-se * @returns Created rule asset saved object */ export const createRuleAssetSavedObject = (overrideParams: Partial) => ({ - 'security-rule': { - ...getPrebuiltRuleMock(), - ...overrideParams, - }, + 'security-rule': getPrebuiltRuleMock(overrideParams), type: 'security-rule', references: [], coreMigrationVersion: '8.6.0', diff --git a/x-pack/test/security_solution_api_integration/tsconfig.json b/x-pack/test/security_solution_api_integration/tsconfig.json index 18e019202355c..d60124a7cc19f 100644 --- a/x-pack/test/security_solution_api_integration/tsconfig.json +++ b/x-pack/test/security_solution_api_integration/tsconfig.json @@ -41,5 +41,6 @@ "@kbn/safer-lodash-set", "@kbn/stack-connectors-plugin", "@kbn/ftr-common-functional-services", + "@kbn/actions-plugin", ] } diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/view_case.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/view_case.ts index 2386d2b0e9585..c1c94583b154d 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cases/view_case.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cases/view_case.ts @@ -529,14 +529,6 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await header.waitUntilLoadingHasFinished(); - await retry.waitFor('update toast exist', async () => { - return await testSubjects.exists('toastCloseButton'); - }); - - await testSubjects.click('toastCloseButton'); - - await header.waitUntilLoadingHasFinished(); - await toggle.click(); await header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts index 94329bc5b48dc..244bc58581052 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/view_case.ts @@ -530,14 +530,6 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await header.waitUntilLoadingHasFinished(); - await retry.waitFor('update toast exist', async () => { - return await testSubjects.exists('toastCloseButton'); - }); - - await testSubjects.click('toastCloseButton'); - - await header.waitUntilLoadingHasFinished(); - await toggle.click(); await header.waitUntilLoadingHasFinished();