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-language-documentation-popover/src/components/documentation_content.test.tsx b/packages/kbn-language-documentation-popover/src/components/documentation_content.test.tsx index e0d7e3c28dbea..17ab14272299e 100644 --- a/packages/kbn-language-documentation-popover/src/components/documentation_content.test.tsx +++ b/packages/kbn-language-documentation-popover/src/components/documentation_content.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; -import { Markdown } from '@kbn/kibana-react-plugin/public'; +import { Markdown } from '@kbn/shared-ux-markdown'; import { LanguageDocumentationPopoverContent } from './documentation_content'; describe('###Documentation popover content', () => { @@ -25,11 +25,15 @@ describe('###Documentation popover content', () => { items: [ { label: 'Section two item 1', - description: , + description: ( + + ), }, { label: 'Section two item 2', - description: , + description: ( + + ), }, ], }, diff --git a/packages/kbn-language-documentation-popover/src/utils/element_to_string.test.tsx b/packages/kbn-language-documentation-popover/src/utils/element_to_string.test.tsx index 42ca61cca472b..abe3430edc947 100644 --- a/packages/kbn-language-documentation-popover/src/utils/element_to_string.test.tsx +++ b/packages/kbn-language-documentation-popover/src/utils/element_to_string.test.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import React from 'react'; -import { Markdown } from '@kbn/kibana-react-plugin/public'; +import { Markdown } from '@kbn/shared-ux-markdown'; import { elementToString } from './element_to_string'; describe('elementToString', () => { @@ -21,7 +21,7 @@ describe('elementToString', () => { }); test('Should convert to string if markdown is passed', () => { - const text = elementToString(); + const text = elementToString(); expect(text).toEqual('## Markdown goes here '); }); @@ -29,7 +29,7 @@ describe('elementToString', () => { const text = elementToString( <>

Meow

- + ); expect(text).toEqual('## Markdown goes here '); diff --git a/packages/kbn-language-documentation-popover/src/utils/element_to_string.ts b/packages/kbn-language-documentation-popover/src/utils/element_to_string.ts index f13bb652f662a..81dcd960737d3 100644 --- a/packages/kbn-language-documentation-popover/src/utils/element_to_string.ts +++ b/packages/kbn-language-documentation-popover/src/utils/element_to_string.ts @@ -20,8 +20,8 @@ export function elementToString(element?: JSX.Element): string { return ''; } const props = element.props; - if (props && 'markdown' in props) { - return String(props.markdown); + if (props && 'markdownContent' in props) { + return String(props.markdownContent); } else if (props && 'children' in props && Array.isArray(props.children)) { return props.children.reduce((text: string, child: React.ReactNode): string => { const validChildren = React.Children.toArray(child).filter(nonNullable); diff --git a/packages/kbn-language-documentation-popover/tsconfig.json b/packages/kbn-language-documentation-popover/tsconfig.json index a0c043b8a15e5..48da6397a6448 100644 --- a/packages/kbn-language-documentation-popover/tsconfig.json +++ b/packages/kbn-language-documentation-popover/tsconfig.json @@ -14,7 +14,7 @@ "kbn_references": [ "@kbn/i18n", "@kbn/test-jest-helpers", - "@kbn/kibana-react-plugin", + "@kbn/shared-ux-markdown", ], "exclude": [ "target/**/*", 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/esql_documentation_sections.tsx b/packages/kbn-text-based-editor/src/esql_documentation_sections.tsx index 2c47f7f309531..015875f977c65 100644 --- a/packages/kbn-text-based-editor/src/esql_documentation_sections.tsx +++ b/packages/kbn-text-based-editor/src/esql_documentation_sections.tsx @@ -7,11 +7,12 @@ */ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { Markdown } from '@kbn/kibana-react-plugin/public'; +import { Markdown } from '@kbn/shared-ux-markdown'; export const initialSection = ( { // 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 ', '')); @@ -147,26 +149,6 @@ export const getDocumentationSections = async (language: string) => { description?: string; items: Array<{ label: string; description?: JSX.Element }>; }> = []; - if (language === 'sql') { - const { - comparisonOperators, - logicalOperators, - mathOperators, - initialSection, - aggregateFunctions, - } = await import('./sql_documentation_sections'); - groups.push({ - label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.howItWorks', { - defaultMessage: 'How it works', - }), - items: [], - }); - groups.push(comparisonOperators, logicalOperators, mathOperators, aggregateFunctions); - return { - groups, - initialSection, - }; - } if (language === 'esql') { const { sourceCommands, diff --git a/packages/kbn-text-based-editor/src/sql_documentation_sections.tsx b/packages/kbn-text-based-editor/src/sql_documentation_sections.tsx deleted file mode 100644 index cb35885ecefc0..0000000000000 --- a/packages/kbn-text-based-editor/src/sql_documentation_sections.tsx +++ /dev/null @@ -1,1125 +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 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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { Markdown } from '@kbn/kibana-react-plugin/public'; - -export const initialSection = ( - -); - -export const comparisonOperators = { - label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.comparisonOperators', { - defaultMessage: 'Comparison operators', - }), - description: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.comparisonOperatorsDocumentationDescription', - { - defaultMessage: `Boolean operator for comparing against one or multiple expressions.`, - } - ), - items: [ - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.equality', - { - defaultMessage: 'Equality', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.nullSafeEquality', - { - defaultMessage: 'Null safe equality (<=>)', - } - ), - description: ( - null AS "equals" - - equals ---------------- -false -\`\`\` -\`\`\` -SELECT null <=> null AS "equals" - - equals ---------------- -true -\`\`\` - `, - description: - 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', - } - )} - /> - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.inequality', - { - defaultMessage: 'Inequality', - } - ), - description: ( - or !=) -\`\`\` -SELECT last_name l FROM "test_emp" -WHERE emp_no <> 10000 ORDER BY emp_no LIMIT 5 -\`\`\` - `, - description: - 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', - } - )} - /> - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.comparison', - { - defaultMessage: 'Comparison', - } - ), - description: ( - , >=) -\`\`\` -SELECT last_name l FROM "test_emp" -WHERE emp_no < 10003 ORDER BY emp_no LIMIT 5 -\`\`\` - `, - description: - 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', - } - )} - /> - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.between', - { - defaultMessage: 'Between', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.nullNotNull', - { - defaultMessage: 'IS NULL and IS NOT NULL', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.inOperator', - { - defaultMessage: 'IN', - } - ), - description: ( - , , ...) -\`\`\` -SELECT last_name l FROM "test_emp" -WHERE emp_no IN (10000, 10001, 10002, 999) ORDER BY emp_no LIMIT 5 -\`\`\` - `, - description: - 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', - } - )} - /> - ), - }, - ], -}; - -export const logicalOperators = { - label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.logicalOperators', { - defaultMessage: 'Logical operators', - }), - description: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.logicalOperatorsDocumentationDescription', - { - defaultMessage: `Boolean operator for evaluating one or two expressions.`, - } - ), - items: [ - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.AndOperator', - { - defaultMessage: 'AND', - } - ), - description: ( - 10000 AND emp_no < 10005 ORDER BY emp_no LIMIT 5 -\`\`\` - `, - description: - 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', - } - )} - /> - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.OrOperator', - { - defaultMessage: 'OR', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.NotOperator', - { - defaultMessage: 'NOT', - } - ), - description: ( - - ), - }, - ], -}; - -export const mathOperators = { - label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.mathOperators', { - defaultMessage: 'Math operators', - }), - description: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.mathOperatorsDocumentationDescription', - { - defaultMessage: `Perform mathematical operations affecting one or two values. The result is a value of numeric type..`, - } - ), - items: [ - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.AddOperator', - { - defaultMessage: 'Add', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.SubtractOperator', - { - defaultMessage: 'Subtract', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.negateOperator', - { - defaultMessage: 'Negate', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.multiplyOperator', - { - defaultMessage: 'Multiply', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.divideOperator', - { - defaultMessage: 'Divide', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.moduloOperator', - { - defaultMessage: 'Modulo or remainder', - } - ), - description: ( - - ), - }, - ], -}; - -export const aggregateFunctions = { - label: i18n.translate('textBasedEditor.query.textBasedLanguagesEditor.aggregateFunctions', { - defaultMessage: 'Aggregate functions', - }), - description: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.aggregateFunctionsDocumentationDescription', - { - defaultMessage: `Functions for computing a single result from a set of input values. Elasticsearch SQL supports aggregate functions only alongside grouping (implicit or explicit).`, - } - ), - items: [ - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.averageFunction', - { - defaultMessage: 'Average', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.countFunction', - { - defaultMessage: 'Count', - } - ), - description: ( - ), all values are considered, including null or missing ones. For COUNT(), null values are not considered. -\`\`\` -SELECT COUNT(*) AS count FROM emp -\`\`\` - `, - description: - 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', - } - )} - /> - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.countAllFunction', - { - defaultMessage: 'Count (All)', - } - ), - description: ( - ) and COUNT(ALL ) are equivalent. - -\`\`\` -COUNT(ALL field_name) -\`\`\` -- a field name. If this field contains only null values, the function returns null. Otherwise, the function ignores null values in this field. -\`\`\` -SELECT COUNT(ALL last_name) AS count_all, COUNT(DISTINCT last_name) count_distinct FROM emp -\`\`\` - `, - description: - 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)', - } - )} - /> - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.countDistinctFunction', - { - defaultMessage: 'Count (Distinct)', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.firstFunction', - { - defaultMessage: 'First / First_value', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.lastFunction', - { - defaultMessage: 'Last / Last_value', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.maxFunction', - { - defaultMessage: 'Max', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.minFunction', - { - defaultMessage: 'Min', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.sumFunction', - { - defaultMessage: 'Sum', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.kurtosisFunction', - { - defaultMessage: 'Kurtosis', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.madFunction', - { - defaultMessage: 'Mad', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.percentileFunction', - { - defaultMessage: 'Percentile', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.percentileRankFunction', - { - defaultMessage: 'Percentile rank', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.skewnessFunction', - { - defaultMessage: 'Skewness', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.stsdevpopFunction', - { - defaultMessage: 'STDDEV_POP', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.stsdevsampFunction', - { - defaultMessage: 'STDDEV_SAMP', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.sumofsquaresFunction', - { - defaultMessage: 'Sum of squares', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.varpopFunction', - { - defaultMessage: 'VAR_POP', - } - ), - description: ( - - ), - }, - { - label: i18n.translate( - 'textBasedEditor.query.textBasedLanguagesEditor.documentation.varsampFunction', - { - defaultMessage: 'VAR_SAMP', - } - ), - description: ( - - ), - }, - ], -}; 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/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts index 49ce940743e70..febb15f4504dd 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts @@ -41,9 +41,6 @@ export async function executeCreateAction({ api: PresentationContainer; }) { const isCompatibleAction = isCreateActionCompatible(core); - const defaultDataView = await deps.dataViews.getDefaultDataView({ - displayErrors: false, - }); const getFallbackDataView = async () => { const indexName = await getIndexForESQLQuery({ dataViews: deps.dataViews }); @@ -52,7 +49,7 @@ export async function executeCreateAction({ return dataView; }; - const dataView = defaultDataView ?? (await getFallbackDataView()); + const dataView = await getFallbackDataView(); if (!isCompatibleAction || !dataView) { throw new IncompatibleActionError(); 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/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 2cbcdfc9416bb..7ded25a070227 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -174,6 +174,11 @@ export const allowedExperimentalValues = Object.freeze({ */ sentinelOneManualHostActionsEnabled: true, + /* + * Enables experimental Crowdstrike integration data to be available in Analyzer + */ + crowdstrikeDataInAnalyzerEnabled: false, + /* * Enables experimental "Updates" tab in the prebuilt rule upgrade flyout. * This tab shows the JSON diff between the installed prebuilt rule 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/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx index 91e30fb9d10b5..553abf36e2d5c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx @@ -14,9 +14,13 @@ export const useIsInvestigateInResolverActionEnabled = (ecsData?: Ecs) => { const sentinelOneDataInAnalyzerEnabled = useIsExperimentalFeatureEnabled( 'sentinelOneDataInAnalyzerEnabled' ); + const crowdstrikeDataInAnalyzerEnabled = useIsExperimentalFeatureEnabled( + 'crowdstrikeDataInAnalyzerEnabled' + ); return useMemo(() => { const fileBeatModules = [ ...(sentinelOneDataInAnalyzerEnabled ? ['sentinel_one_cloud_funnel', 'sentinel_one'] : []), + ...(crowdstrikeDataInAnalyzerEnabled ? ['crowdstrike'] : []), ] as const; const agentType = get(['agent', 'type', 0], ecsData); @@ -36,5 +40,5 @@ export const useIsInvestigateInResolverActionEnabled = (ecsData?: Ecs) => { processEntityIds != null && processEntityIds.length === 1 && firstProcessEntityId !== ''; return isAcceptedAgentType && hasProcessEntityId; - }, [ecsData, sentinelOneDataInAnalyzerEnabled]); + }, [crowdstrikeDataInAnalyzerEnabled, ecsData, sentinelOneDataInAnalyzerEnabled]); }; 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/endpoint/routes/resolver/entity/utils/supported_schemas.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/supported_schemas.ts index de95c8fc9f022..766a92904f4bc 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/supported_schemas.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity/utils/supported_schemas.ts @@ -35,11 +35,13 @@ export const getSupportedSchemas = ( experimentalFeatures: ExperimentalFeatures | undefined ): SupportedSchema[] => { const sentinelOneDataInAnalyzerEnabled = experimentalFeatures?.sentinelOneDataInAnalyzerEnabled; + const crowdstrikeDataInAnalyzerEnabled = experimentalFeatures?.crowdstrikeDataInAnalyzerEnabled; const supportedFileBeatDataSets = [ ...(sentinelOneDataInAnalyzerEnabled ? ['sentinel_one_cloud_funnel.event', 'sentinel_one.alert'] : []), + ...(crowdstrikeDataInAnalyzerEnabled ? ['crowdstrike.falcon', 'crowdstrike.fdr'] : []), ]; return [ 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 d9024b013ae93..e8941361bf203 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -5466,42 +5466,6 @@ "textBasedEditor.query.textBasedLanguagesEditor.lineCount": "{count} {count, plural, one {ligne} many {lignes} other {lignes}}", "textBasedEditor.query.textBasedLanguagesEditor.lineNumber": "Ligne {lineNumber}", "textBasedEditor.query.textBasedLanguagesEditor.warningCount": "{count} {count, plural, one {avertissement} many {avertissements} other {avertissements}}", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.addOperator.markdown": "### Add (+)\n```\nSELECT 1 + 1 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.andOperator.markdown": "### AND\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no > 10000 AND emp_no < 10005 ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.averageFunction.markdown": "### AVG\nRetourne la moyenne (moyenne arithmétique) des valeurs entrées.\n```\nAVG(numeric_field)\n```\n- champ numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n```\nSELECT AVG(salary) AS avg FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.between.markdown": "### Between\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no BETWEEN 9990 AND 10003 ORDER BY emp_no\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.comparison.markdown": "### Comparison (<, <=, >, >=)\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no < 10003 ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countAllFunction.markdown": "### Count (All)\nRenvoie le nombre total de toutes les valeurs non nulles en entrée. COUNT() et COUNT(ALL ) sont équivalents.\n\n```\nCOUNT(ALL field_name) \n```\n- nom du champ. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n```\nSELECT COUNT(ALL last_name) AS count_all, COUNT(DISTINCT last_name) count_distinct FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countDistinctFunction.markdown": "### Count (Distinct)\nRenvoie le nombre total de valeurs non nulles distinctes dans les valeurs en entrée.\n\n```\nCOUNT(DISTINCT field_name)\n```\n- Entrée : un nom de champ.\n- Sortie : une valeur numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n```\nSELECT COUNT(DISTINCT hire_date) unique_hires, COUNT(hire_date) AS hires FROM emp\n\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countFunction.markdown": "### Count\nRenvoie le nombre total de valeurs en entrée.\n\n\n```\nCOUNT(expression)\n```\n- expression. Nom de champ, caractère générique (*) ou n'importe quelle valeur numérique. Pour COUNT(*) ou COUNT(), toutes les valeurs sont prises en compte, même celles manquantes ou nulles. Pour COUNT(), les valeurs nulles ne sont pas prises en compte.\n```\nSELECT COUNT(*) AS count FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.divideOperator.markdown": "### Divide (/)\n```\nSELECT 6 / 3 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.equality.markdown": "### Equality (=)\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no = 10000 LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.firstFunction.markdown": "### FIRST / FIRST_VALUE\nRenvoie la première valeur non nulle (si elle existe) de la colonne d'entrée field_name triée selon la colonne ordering_field_name. Si la valeur ordering_field_name n'est pas fournie, seule la colonne field_name est utilisée pour le tri. \n\n```\nFIRST(\n field_name \n [, ordering_field_name])\n```\n- Nom du champ : champ cible de l'agrégation\n- ordering_field_name : champ facultatif utilisé pour le tri.\n\n```\nSELECT gender, FIRST(first_name, birth_date) FROM emp GROUP BY gender ORDER BY gender\n```\n\n- FIRST ne peut pas être utilisé dans une clause HAVING.\n- FIRST ne peut pas être utilisé avec des colonnes de type texte, sauf si le champ est aussi enregistré comme mot-clé.\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.inequality.markdown": "### Inequality (<> or !=)\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no <> 10000 ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.inOperator.markdown": "### IN (, , ...)\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no IN (10000, 10001, 10002, 999) ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.kurtosisFunction.markdown": "### KURTOSIS\nQuantifier la forme de la distribution des valeurs en entrée dans le champ field_name.\n\n```\nKURTOSIS(field_name) \n```\n- champ numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, KURTOSIS(salary) AS k FROM emp\n```\n\n- KURTOSIS ne peut pas être utilisé en plus des fonctions ou des opérateurs scalaires, uniquement sur un champ. \n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.lastFunction.markdown": "### LAST / LAST_VALUE\nInverse de FIRST/FIRST_VALUE. Renvoie la dernière valeur non nulle (si elle existe) de la colonne d'entrée field_name triée par ordre croissant selon la colonne ordering_field_name. Si la valeur ordering_field_name n'est pas fournie, seule la colonne field_name est utilisée pour le tri. \n\n```\nLAST(\n field_name \n [, ordering_field_name])\n```\n- Nom du champ : champ cible de l'agrégation\n- ordering_field_name : champ facultatif utilisé pour le tri.\n```\nSELECT gender, LAST(first_name) FROM emp GROUP BY gender ORDER BY gender\n```\n- LAST ne peut pas être utilisé dans une clause HAVING.\n- LAST ne peut pas être utilisé avec des colonnes de type texte, sauf si le champ est aussi enregistré comme mot-clé.\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.madFunction.markdown": "### MAD\nMesure la variabilité des valeurs d'entrée dans le champ field_name.\n\n```\nMAD(field_name) \n```\n- champ numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, AVG(salary) AS avg, MAD(salary) AS mad FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.markdown": "## À propos d'Elasticsearch SQL\n\nUtilisez Elasticsearch SQL pour rechercher et agréger les données dans Elasticsearch. Ce langage de requête fournit une recherche full text avec une syntaxe connue. Voici un exemple de requête :\n \n```\nSELECT * FROM library \nORDER BY page_count DESC LIMIT 5\n```\n \nElasticsearch SQL :\n\n- Fournit un jeu complet d'opérateurs et de fonctions intégrés.\n- Suit la terminologie et les conventions SQL.\n- Accepte une commande par ligne. Une commande est une séquence de jetons terminée par la fin du flux d'entrée\n \n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.maxFunction.markdown": "### MAX\nRetourne la valeur maximale de toutes les valeurs en entrée dans le champ field_name.\n\n```\nMAX(field_name) \n```\n- champ numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n\n```\nSELECT MAX(salary) AS max FROM emp\n```\n\n- MAX sur un champ de type texte ou mot-clé est traduit en FIRST/FIRST_VALUE et ne peut donc pas être utilisé dans la clause HAVING.\n\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.minFunction.markdown": "### MIN\nRetourne la valeur minimale de toutes les valeurs en entrée dans le champ field_name.\n\n```\nMIN(field_name) \n```\n- champ numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n\n```\nSELECT MIN(salary) AS min FROM emp\n```\n\n- MIN sur un champ de type texte ou mot-clé est traduit en FIRST/FIRST_VALUE et ne peut donc pas être utilisé dans la clause HAVING.\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.moduloOperator.markdown": "### Modulo or remainder(%)\n```\nSELECT 5 % 2 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.multiplyOperator.markdown": "### Multiply (*)\n```\nSELECT 2 * 3 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.negateOperator.markdown": "### Negate (unary -)\n```\nSELECT - 1 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.notOperator.markdown": "### NOT\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE NOT emp_no = 10000 LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.nullNotNull.markdown": "### IS NULL/IS NOT NULL\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no IS NOT NULL AND gender IS NULL\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.nullSafeEquality.markdown": "### Null safe equality:\n```\nSELECT 'elastic' <=> null AS \"equals\"\n\n égal\n---------------\nfaux\n```\n```\nSELECT null <=> null AS \"equals\"\n\n égal\n---------------\nvrai\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.orOperator.markdown": "### OR\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no < 10003 OR emp_no = 10005 ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.percentileFunction.markdown": "### PERCENTILE\nRetourne le centile n (représenté par le paramètre numeric_exp) des valeurs en entrée dans le champ field_name.\n\n```\nPERCENTILE(\n field_name, \n percentile[, \n method[, \n method_parameter]])\n```\n- field_name : champ numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n- centile : expression numérique (doit être une constante et ne doit pas être basé sur un champ). Si la valeur est nulle, la fonction renvoie une valeur nulle.\n- method : chaîne littérale facultative pour l'algorithme de centile. Valeurs possibles : tdigest ou hdr. La valeur par défaut est tdigest.\n- method_parameter : numérique littéral facultatif qui configure l'algorithme de centile. Configure la compression pour tdigest ou number_of_significant_value_digits pour hdr. La valeur par défaut est la même que celle de l'algorithme de sauvegarde.\n\n```\nSELECT\n languages,\n PERCENTILE(salary, 97.3, 'tdigest', 100.0) AS \"97.3_TDigest\",\n PERCENTILE(salary, 97.3, 'hdr', 3) AS \"97.3_HDR\"\nFROM emp\nGROUP BY languages\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.percentileRankFunction.markdown": "### PERCENTILE_RANK\nRetourne le rang centile n (représenté par le paramètre numeric_exp) des valeurs en entrée dans le champ field_name.\n\n```\nPERCENTILE_RANK(\n field_name, \n value[, \n method[, \n method_parameter]]) \n```\n- field_name : champ numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n- centile : expression numérique (doit être une constante et ne doit pas être basé sur un champ). Si la valeur est nulle, la fonction renvoie une valeur nulle.\n- method : chaîne littérale facultative pour l'algorithme de centile. Valeurs possibles : tdigest ou hdr. La valeur par défaut est tdigest.\n- method_parameter : numérique littéral facultatif qui configure l'algorithme de centile. Configure la compression pour tdigest ou number_of_significant_value_digits pour hdr. La valeur par défaut est la même que celle de l'algorithme de sauvegarde.\n\n```\nSELECT\n languages,\n ROUND(PERCENTILE_RANK(salary, 65000, 'tdigest', 100.0), 2) AS \"rank_TDigest\",\n ROUND(PERCENTILE_RANK(salary, 65000, 'hdr', 3), 2) AS \"rank_HDR\"\nFROM emp\nGROUP BY languages\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.skewnessFunction.markdown": "### SKEWNESS\nQuantifier la distribution asymétrique des valeurs en entrée dans le champ field_name.\n\n```\nSKEWNESS(field_name) \n```\n- field_name : champ numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, SKEWNESS(salary) AS s FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.stsdevpopFunction.markdown": "### STDDEV_POP\nRetourne l'écart type de population des valeurs en entrée dans le champ field_name.\n\n```\nSTDDEV_POP(field_name) \n```\n- field_name : champ numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, STDDEV_POP(salary) AS stddev FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.stsdevsampFunction.markdown": "### STDDEV_SAMP\nRetourne l'écart type de l'échantillon des valeurs en entrée dans le champ field_name.\n\n```\nSTDDEV_SAMP(field_name) \n```\n- field_name : champ numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, STDDEV_SAMP(salary) AS stddev FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.subtractOperator.markdown": "### Subtract (infix -)\n```\nSELECT 1 - 1 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.sumFunction.markdown": "### SUM\nRetourne la somme des valeurs en entrée dans le champ field_name.\n\n```\nSUM(field_name) \n```\n- champ numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n\n```\nSELECT SUM(salary) AS sum FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.sumofsquaresFunction.markdown": "### SUM_OF_SQUARES\nRetourne la somme des carrés des valeurs en entrée dans le champ field_name.\n\n```\nSUM_OF_SQUARES(field_name) \n```\n- field_name : champ numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, SUM_OF_SQUARES(salary) AS sumsq\n FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.varpopFunction.markdown": "### VAR_POP\nRetourne la variance de population des valeurs en entrée dans le champ field_name.\n\n```\nVAR_POP(field_name) \n```\n- field_name : champ numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, VAR_POP(salary) AS varpop FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.varsampFunction.markdown": "### VAR_SAMP\nRetourne la variance de l'échantillon de valeurs en entrée dans le champ field_name.\n\n```\nVAR_SAMP(field_name) \n```\n- field_name : champ numérique. Si ce champ contient uniquement des valeurs nulles, la fonction renvoie zéro. Sinon, la fonction ignore les valeurs nulles dans ce champ.\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, VAR_SAMP(salary) AS varsamp FROM emp\n```\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.absFunction.markdown": "### ABS\nRenvoie la valeur absolue.\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL abs_height = ABS(0.0 - height)\n```\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.acosFunction.markdown": "### ACOS\nFonction trigonométrique cosinus inverse.\n\n```\nROW a=.9\n| EVAL acos=ACOS(a)\n```\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.asinFunction.markdown": "### ASIN\nFonction trigonométrique sinus inverse.\n\n```\nROW a=.9\n| EVAL asin=ASIN(a)\n```\n ", @@ -5592,49 +5556,10 @@ "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.toVersionFunction.markdown": "### TO_VERSION\nConvertit une chaîne d'entrée en une valeur de version. Par exemple :\n\n```\nROW v = TO_VERSION(\"1.2.3\")\n```\n\nRenvoi :\n\n```\n1.2.3\n```\n\nAlias : TO_VER\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.trimFunction.markdown": "### TRIM\nSupprime les espaces de début et de fin d'une chaîne.\n\n```\nROW message = \" some text \", color = \" red \"\n| EVAL message = TRIM(message)\n| EVAL color = TRIM(color)\n```\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.where.markdown": "### WHERE\nUtilisez `WHERE` afin d'obtenir un tableau qui comprend toutes les lignes du tableau d'entrée pour lesquelles la condition fournie est évaluée à `true` :\n \n```\nFROM employees\n| KEEP first_name, last_name, still_hired\n| WHERE still_hired == true\n```\n\n#### Opérateurs\n\nPour obtenir un aperçu des opérateurs pris en charge, consultez la section **Opérateurs**.\n\n#### Fonctions\n`WHERE` prend en charge diverses fonctions de calcul des valeurs. Pour en savoir plus, consultez la section **Fonctions**.\n ", - "textBasedEditor.query.textBasedLanguagesEditor.aggregateFunctions": "Fonctions agrégées", - "textBasedEditor.query.textBasedLanguagesEditor.aggregateFunctionsDocumentationDescription": "Fonctions permettant de calculer un résultat unique à partir d'un ensemble de valeurs d'entrée. Elasticsearch SQL ne prend en charge les fonctions agrégées que parallèlement au regroupement (implicite ou explicite).", "textBasedEditor.query.textBasedLanguagesEditor.aggregationFunctions": "Fonctions d'agrégation", "textBasedEditor.query.textBasedLanguagesEditor.aggregationFunctionsDocumentationESQLDescription": "Ces fonctions peuvent être utilisées avec STATS...BY :", "textBasedEditor.query.textBasedLanguagesEditor.commandsDescription": "Une commande source produit un tableau, habituellement avec des données issues d'Elasticsearch. ES|QL est compatible avec les commandes sources suivantes.", - "textBasedEditor.query.textBasedLanguagesEditor.comparisonOperators": "Opérateurs de comparaison", - "textBasedEditor.query.textBasedLanguagesEditor.comparisonOperatorsDocumentationDescription": "Opérateur booléen pour une comparaison par rapport à une ou plusieurs expressions.", "textBasedEditor.query.textBasedLanguagesEditor.disableWordWrapLabel": "Désactiver l'encadrement par des barres", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.AddOperator": "Ajouter", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.AndOperator": "AND", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.averageFunction": "Moyenne", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.between": "Entre", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.comparison": "Comparaison", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countAllFunction": "Count (All)", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countDistinctFunction": "Count (Distinct)", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countFunction": "Décompte", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.divideOperator": "Divide", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.equality": "Equality", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.firstFunction": "First / First_value", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.inequality": "Inequality", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.inOperator": "IN", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.kurtosisFunction": "Kurtosis", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.lastFunction": "Last / Last_value", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.madFunction": "Mad", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.maxFunction": "Max.", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.minFunction": "Min.", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.moduloOperator": "Modulo or remainder", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.multiplyOperator": "Multiplier", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.negateOperator": "Negate", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.NotOperator": "NON", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.nullNotNull": "IS NULL et IS NOT NULL", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.nullSafeEquality": "Null safe equality (<=>)", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.OrOperator": "OR", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.percentileFunction": "Centile", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.percentileRankFunction": "Rang centile", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.skewnessFunction": "Skewness", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.stsdevpopFunction": "STDDEV_POP", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.stsdevsampFunction": "STDDEV_SAMP", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.SubtractOperator": "Subtract", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.sumFunction": "Somme", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.sumofsquaresFunction": "Sum of squares", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.varpopFunction": "VAR_POP", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.varsampFunction": "VAR_SAMP", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.absFunction": "ABS", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.acosFunction": "ACOS", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.asinFunction": "ASIN", @@ -5731,11 +5656,6 @@ "textBasedEditor.query.textBasedLanguagesEditor.expandTooltip": "Développer l’éditeur de requête", "textBasedEditor.query.textBasedLanguagesEditor.functions": "Fonctions", "textBasedEditor.query.textBasedLanguagesEditor.functionsDocumentationESQLDescription": "Les fonctions sont compatibles avec \"ROW\" (Ligne), \"EVAL\" (Évaluation) et \"WHERE\" (Où).", - "textBasedEditor.query.textBasedLanguagesEditor.howItWorks": "Fonctionnement", - "textBasedEditor.query.textBasedLanguagesEditor.logicalOperators": "Opérateurs logiques", - "textBasedEditor.query.textBasedLanguagesEditor.logicalOperatorsDocumentationDescription": "Opérateur booléen permettant d'évaluer une ou deux expressions.", - "textBasedEditor.query.textBasedLanguagesEditor.mathOperators": "Opérateurs mathématiques", - "textBasedEditor.query.textBasedLanguagesEditor.mathOperatorsDocumentationDescription": "Effectuer des opérations mathématiques affectant une ou deux valeurs. Le résultat est une valeur de type numérique.", "textBasedEditor.query.textBasedLanguagesEditor.MinimizeEditor": "Réduire l'éditeur", "textBasedEditor.query.textBasedLanguagesEditor.minimizeTooltip": "Réduire l’éditeur de requête", "textBasedEditor.query.textBasedLanguagesEditor.operators": "Opérateurs", @@ -34402,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", @@ -35562,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 939a34380cd54..d808deca26b98 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5481,42 +5481,6 @@ "textBasedEditor.query.textBasedLanguagesEditor.lineCount": "{count} {count, plural, other {行}}", "textBasedEditor.query.textBasedLanguagesEditor.lineNumber": "行{lineNumber}", "textBasedEditor.query.textBasedLanguagesEditor.warningCount": "{count} {count, plural, other {警告}}", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.addOperator.markdown": "### 加算(+)\n```\nSELECT 1 + 1 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.andOperator.markdown": "### AND\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no > 10000 AND emp_no < 10005 ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.averageFunction.markdown": "### AVG\n入力値の平均(算術平均)が返されます。\n```\nAVG(numeric_field)\n```\n- 数値フィールド。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n```\nSELECT AVG(salary) AS avg FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.between.markdown": "### Between\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no BETWEEN 9990 AND 10003 ORDER BY emp_no\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.comparison.markdown": "### 比較(<、<=、>、>=)\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no < 10003 ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countAllFunction.markdown": "### Count (All)\nすべてのヌル以外の入力値の合計数(カウント)が返されます。COUNT() and COUNT(ALL ) are equivalent.\n\n```\nCOUNT(ALL field_name) \n```\n- フィールド名。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n```\nSELECT COUNT(ALL last_name) AS count_all, COUNT(DISTINCT last_name) count_distinct FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countDistinctFunction.markdown": "### Count (Distinct)\n入力値の重複しないヌル以外の値の合計数が返されます。\n\n```\nCOUNT(DISTINCT field_name)\n```\n- 入力:フィールド名。\n- 出力:数値。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n```\nSELECT COUNT(DISTINCT hire_date) unique_hires, COUNT(hire_date) AS hires FROM emp\n\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countFunction.markdown": "### Count\n入力値の合計数(カウント)が返されます。\n\n\n```\nCOUNT(式)\n```\n- 式。フィールド名、ワイルドカード(*)、または任意の数値。COUNT(*)またはCOUNT()の場合、ヌルや不足している値を含むすべての値が考慮されます。COUNT()の場合、ヌル値は考慮されません。\n```\nSELECT COUNT(*) AS count FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.divideOperator.markdown": "### 除算(/)\n```\nSELECT 6 / 3 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.equality.markdown": "### 等号(=)\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no = 10000 LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.firstFunction.markdown": "### FIRST / FIRST_VALUE\nfield_name入力列の最初のヌル以外の値(存在する場合)が、ordering_field_name列でソートされて返されます。ordering_field_nameが指定されていない場合は、field_name列のみがソートで使用されます。\n\n```\nFIRST(\n field_name \n [, ordering_field_name])\n```\n- フィールド名:集計の対象フィールド\n- ordering_field_name:並べ替えで使用される任意のフィールド。\n\n```\nSELECT gender, FIRST(first_name, birth_date) FROM emp GROUP BY gender ORDER BY gender\n```\n\n- FIRSTはHAVING句で使用できません。\n- フィールドがキーワードとして保存されていない場合、FIRSTはテキスト型の列で使用できません。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.inequality.markdown": "### 不等号(<>または!=)\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no <> 10000 ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.inOperator.markdown": "### IN (, , ...)\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no IN (10000, 10001, 10002, 999) ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.kurtosisFunction.markdown": "### KURTOSIS\nfield_nameフィールドの入力値の分布の形状を定量化します。\n\n```\nKURTOSIS(field_name) \n```\n- 数値フィールド。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, KURTOSIS(salary) AS k FROM emp\n```\n\n- KURTOSISは、スカラー関数または演算子に対して使用できません。直接フィールドに対してのみ使用できます。 \n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.lastFunction.markdown": "### LAST / LAST_VALUE\nFIRST/FIRST_VALUEの反転です。field_name入力列の最後のヌル以外の値(存在する場合)が、ordering_field_name列で降順にソートされて返されます。ordering_field_nameが指定されていない場合は、field_name列のみがソートで使用されます。 \n\n```\nLAST(\n field_name \n [, ordering_field_name])\n```\n- フィールド名:集計の対象フィールド\n- ordering_field_name:並べ替えで使用される任意のフィールド。\n```\nSELECT gender, LAST(first_name) FROM emp GROUP BY gender ORDER BY gender\n```\n- LASTはHAVING句で使用できません。\n- フィールドがキーワードとして保存されていない場合、LASTはテキスト型の列で使用できません。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.madFunction.markdown": "### MAD\nfield_nameフィールドの入力値の変化を測定します。\n\n```\nMAD(field_name) \n```\n- 数値フィールド。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, AVG(salary) AS avg, MAD(salary) AS mad FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.markdown": "## Elasticsearch SQLについてさらに詳しく\n\nElasticsearch SQLを使用すると、Elasticsearch内部でデータの検索と集計ができます。このクエリ言語では、使い慣れた構文で全文検索が可能です。クエリの例は次のとおりです。\n \n```\nSELECT * FROM library \nORDER BY page_count DESC LIMIT 5\n```\n \nElasticsearch SQL | \n\n- 演算子と関数の包括的なセットが組み込まれています。\n- SQLの用語と規則に従います。\n- 各行に1つのコマンドを入力できます。コマンドは、入力ストリームの最後に終了する一連の文字です。\n \n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.maxFunction.markdown": "### MAX\nfield_nameフィールドの入力値の最大値が返されます。\n\n```\nMAX(field_name) \n```\n- 数値フィールド。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n\n```\nSELECT MAX(salary) AS max FROM emp\n```\n\nテキスト型やキーワード型のフィールドに対するMAXはFIRST/FIRST_VALUEに変換されるため、HAVING句では使用できません。\n\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.minFunction.markdown": "### MIN\nfield_nameフィールドの入力値の最小値が返されます。\n\n```\nMIN(field_name) \n```\n- 数値フィールド。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n\n```\nSELECT MIN(salary) AS min FROM emp\n```\n\nテキスト型やキーワード型のフィールドに対するINはFIRST/FIRST_VALUEに変換されるため、HAVING句では使用できません。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.moduloOperator.markdown": "### Moduloまたは剰余(%)\n```\nSELECT 5 % 2 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.multiplyOperator.markdown": "### 乗算(*)\n```\nSELECT 2 * 3 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.negateOperator.markdown": "### 否定(単項-)\n```\nSELECT - 1 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.notOperator.markdown": "### NOT\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE NOT emp_no = 10000 LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.nullNotNull.markdown": "### IS NULL/IS NOT NULL\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no IS NOT NULL AND gender IS NULL\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.nullSafeEquality.markdown": "### Null宇宙船演算子\n```\nSELECT 'elastic' <=> null AS \"equals\"\n\n 一致する\n---------------\nfalse\n```\n```\nSELECT null <=> null AS \"equals\"\n\n 一致する\n---------------\ntrue\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.orOperator.markdown": "### OR\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no < 10003 OR emp_no = 10005 ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.percentileFunction.markdown": "### PERCENTILE\nfield_nameフィールドの入力値の(numeric_expパラメーターで表現された)第nパーセンタイルが返されます。\n\n```\nPERCENTILE(\n field_name, \n percentile[, \n method[, \n method_parameter]])\n```\n- field_name : 数値フィールド。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n- percentile : 数式(フィールドに基づくのではなく、定数でなければなりません)。ヌルの場合、ヌルが返されます。\n- method : パーセンタイルアルゴリズムの任意の文字列リテラル。使用可能な値:tdigestまたはhdr。デフォルトはtdigestです。\n- method_parameter:パーセンタイルアルゴリズムを構成する任意の数値リテラル。tdigestの圧縮またはhdrのnumber_of_significant_value_digitsを構成します。デフォルトは、基本のアルゴリズムと同じです。\n\n```\nSELECT\n languages,\n PERCENTILE(salary, 97.3, 'tdigest', 100.0) AS \"97.3_TDigest\",\n PERCENTILE(salary, 97.3, 'hdr', 3) AS \"97.3_HDR\"\nFROM emp\nGROUP BY languages\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.percentileRankFunction.markdown": "### PERCENTILE_RANK\nfield_nameフィールドの入力値の(numeric_expパラメーターで表現された)第nパーセンタイルランクが返されます。\n\n```\nPERCENTILE_RANK(\n field_name, \n value[, \n method[, \n method_parameter]]) \n```\n- field_name : 数値フィールド。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n- percentile : 数式(フィールドに基づくのではなく、定数でなければなりません)。ヌルの場合、ヌルが返されます。\n- method : パーセンタイルアルゴリズムの任意の文字列リテラル。使用可能な値:tdigestまたはhdr。デフォルトはtdigestです。\n- method_parameter:パーセンタイルアルゴリズムを構成する任意の数値リテラル。tdigestの圧縮またはhdrのnumber_of_significant_value_digitsを構成します。デフォルトは、基本のアルゴリズムと同じです。\n\n```\nSELECT\n languages,\n ROUND(PERCENTILE_RANK(salary, 65000, 'tdigest', 100.0), 2) AS \"rank_TDigest\",\n ROUND(PERCENTILE_RANK(salary, 65000, 'hdr', 3), 2) AS \"rank_HDR\"\nFROM emp\nGROUP BY languages\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.skewnessFunction.markdown": "### SKEWNESS\nfield_nameフィールドの入力値の非対称分布を定量化します。\n\n```\nSKEWNESS(field_name) \n```\n- field_name : 数値フィールド。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, SKEWNESS(salary) AS s FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.stsdevpopFunction.markdown": "### STDDEV_POP\nfield_nameフィールドの入力値の母標準偏差が返されます。\n\n```\nSTDDEV_POP(field_name) \n```\n- field_name : 数値フィールド。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, STDDEV_POP(salary) AS stddev FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.stsdevsampFunction.markdown": "### STDDEV_SAMP\nfield_nameフィールドの入力値の標本標準偏差が返されます。\n\n```\nSTDDEV_SAMP(field_name) \n```\n- field_name : 数値フィールド。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, STDDEV_SAMP(salary) AS stddev FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.subtractOperator.markdown": "### 減算(infix -)\n```\nSELECT 1 - 1 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.sumFunction.markdown": "### SUM\nfield_nameフィールドの入力値の合計が返されます。\n\n```\nSUM(field_name) \n```\n- 数値フィールド。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n\n```\nSELECT SUM(salary) AS sum FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.sumofsquaresFunction.markdown": "### SUM_OF_SQUARES\nfield_nameフィールドの入力値の平方根の合計が返されます。\n\n```\nSUM_OF_SQUARES(field_name) \n```\n- field_name : 数値フィールド。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, SUM_OF_SQUARES(salary) AS sumsq\n FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.varpopFunction.markdown": "### VAR_POP\nfield_nameフィールドの入力値の母分散が返されます。\n\n```\nVAR_POP(field_name) \n```\n- field_name : 数値フィールド。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, VAR_POP(salary) AS varpop FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.varsampFunction.markdown": "### VAR_SAMP\nfield_nameフィールドの入力値の標本分散が返されます。\n\n```\nVAR_SAMP(field_name) \n```\n- field_name : 数値フィールド。このフィールドにヌル値のみが入力されている場合、関数によってヌルが返されます。そうでない場合は、このフィールドのヌル値は無視されます。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, VAR_SAMP(salary) AS varsamp FROM emp\n```\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.absFunction.markdown": "### ABS\n絶対値を返します。\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL abs_height = ABS(0.0 - height)\n```\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.acosFunction.markdown": "### ACOS\n余弦三角関数を反転します。\n\n```\nROW a=.9\n| EVAL acos=ACOS(a)\n```\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.asinFunction.markdown": "### ASIN\n正弦三角関数を反転します。\n\n```\nROW a=.9\n| EVAL asin=ASIN(a)\n```\n ", @@ -5607,49 +5571,10 @@ "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.toVersionFunction.markdown": "### TO_VERSION\n入力文字列をバージョン値に変換します。例:\n\n```\nROW v = TO_VERSION(\"1.2.3\")\n```\n\n次の結果を返します。\n\n```\n1.2.3\n```\n\nエイリアス:TO_VER\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.trimFunction.markdown": "### TRIM\n文字列から先頭と末尾の空白を削除します。\n\n```\nROW message = \" some text \", color = \" red \"\n| EVAL message = TRIM(message)\n| EVAL color = TRIM(color)\n```\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.where.markdown": "### WHERE\nWHEREを使用すると、入力テーブルから、指定した条件がtrueと評価されるすべての行を含むテーブルを作成します。\n \n```\nFROM employees\n| KEEP first_name, last_name, still_hired\n| WHERE still_hired == true\n```\n\n#### 演算子\n\nサポートされている演算子の概要については、**演算子**を参照してください。\n\n#### 関数\nWHEREは値を計算するためのさまざまな関数をサポートしています。**関数**をクリックすると詳細が表示されます。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.aggregateFunctions": "集計関数", - "textBasedEditor.query.textBasedLanguagesEditor.aggregateFunctionsDocumentationDescription": "複数の入力値のセットから単一の結果を計算するための関数。Elasticsearch SQLでは、集計関数は(明示的または暗黙的な)グループ化を行った場合にのみ使用できます。", "textBasedEditor.query.textBasedLanguagesEditor.aggregationFunctions": "集約関数", "textBasedEditor.query.textBasedLanguagesEditor.aggregationFunctionsDocumentationESQLDescription": "これらの関数はSTATS...BYで使用できます。", "textBasedEditor.query.textBasedLanguagesEditor.commandsDescription": "通常、ソースコマンドはElasticsearchのデータを使ってテーブルを生成します。ES|QLは以下のソースコマンドをサポートしています。", - "textBasedEditor.query.textBasedLanguagesEditor.comparisonOperators": "比較演算子", - "textBasedEditor.query.textBasedLanguagesEditor.comparisonOperatorsDocumentationDescription": "1つ以上の式に対して比較を行うためのブール演算子。", "textBasedEditor.query.textBasedLanguagesEditor.disableWordWrapLabel": "パイプによるラップを無効化", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.AddOperator": "追加", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.AndOperator": "AND", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.averageFunction": "平均", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.between": "Between", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.comparison": "比較", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countAllFunction": "Count (All)", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countDistinctFunction": "Count (Distinct)", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countFunction": "カウント", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.divideOperator": "除算", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.equality": "等号", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.firstFunction": "First / First_value", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.inequality": "不等号", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.inOperator": "IN", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.kurtosisFunction": "Kurtosis", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.lastFunction": "Last / Last_value", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.madFunction": "Mad", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.maxFunction": "最高", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.minFunction": "最低", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.moduloOperator": "Moduloまたは剰余", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.multiplyOperator": "乗算", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.negateOperator": "否定", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.NotOperator": "NOT", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.nullNotNull": "IS NULLおよびIS NOT NULL", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.nullSafeEquality": "Null宇宙船演算子(<=>)", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.OrOperator": "OR", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.percentileFunction": "パーセンタイル", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.percentileRankFunction": "パーセンタイル順位", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.skewnessFunction": "Skewness", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.stsdevpopFunction": "STDDEV_POP", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.stsdevsampFunction": "STDDEV_SAMP", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.SubtractOperator": "減算", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.sumFunction": "合計", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.sumofsquaresFunction": "平方和", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.varpopFunction": "VAR_POP", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.varsampFunction": "VAR_SAMP", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.absFunction": "ABS", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.acosFunction": "ACOS", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.asinFunction": "ASIN", @@ -5746,11 +5671,6 @@ "textBasedEditor.query.textBasedLanguagesEditor.expandTooltip": "クエリエディターを展開", "textBasedEditor.query.textBasedLanguagesEditor.functions": "関数", "textBasedEditor.query.textBasedLanguagesEditor.functionsDocumentationESQLDescription": "関数はROW、EVAL、WHEREでサポートされています。", - "textBasedEditor.query.textBasedLanguagesEditor.howItWorks": "仕組み", - "textBasedEditor.query.textBasedLanguagesEditor.logicalOperators": "論理演算子", - "textBasedEditor.query.textBasedLanguagesEditor.logicalOperatorsDocumentationDescription": "1つまたは2つの式を評価するためのブール演算子。", - "textBasedEditor.query.textBasedLanguagesEditor.mathOperators": "数学演算子", - "textBasedEditor.query.textBasedLanguagesEditor.mathOperatorsDocumentationDescription": "1つまたは2つの値に影響する数学演算を実行します。結果は数値型の値です。", "textBasedEditor.query.textBasedLanguagesEditor.MinimizeEditor": "エディターを最小化", "textBasedEditor.query.textBasedLanguagesEditor.minimizeTooltip": "クエリエディターを縮小", "textBasedEditor.query.textBasedLanguagesEditor.operators": "演算子", @@ -34402,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": "すべての実行中のプロセスを表示", @@ -35562,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 c3c0f094a3ee9..c6acbb99e9911 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5574,42 +5574,6 @@ "textBasedEditor.query.textBasedLanguagesEditor.lineCount": "{count} {count, plural, other {行}}", "textBasedEditor.query.textBasedLanguagesEditor.lineNumber": "第 {lineNumber} 行", "textBasedEditor.query.textBasedLanguagesEditor.warningCount": "{count} {count, plural, other {警告}}", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.addOperator.markdown": "### Add (+)\n```\nSELECT 1 + 1 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.andOperator.markdown": "### AND\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no > 10000 AND emp_no < 10005 ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.averageFunction.markdown": "### AVG\n返回输入值的算术平均值。\n```\nAVG(numeric_field)\n```\n- 数字字段。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n```\nSELECT AVG(salary) AS avg FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.between.markdown": "### Between\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no BETWEEN 9990 AND 10003 ORDER BY emp_no\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.comparison.markdown": "### Comparison (<, <=, >, >=)\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no < 10003 ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countAllFunction.markdown": "### Count (All)\n返回所有非 null 输入值的总数(计数)。COUNT() 与 COUNT(ALL ) 等价。\n\n```\nCOUNT(ALL field_name) \n```\n- 字段名称。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n```\nSELECT COUNT(ALL last_name) AS count_all, COUNT(DISTINCT last_name) count_distinct FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countDistinctFunction.markdown": "### Count (Distinct)\n返回输入值中的不同非 null 值的总数。\n\n```\nCOUNT(DISTINCT field_name)\n```\n- 输入:字段名称。\n- 输出:数字值。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n```\nSELECT COUNT(DISTINCT hire_date) unique_hires, COUNT(hire_date) AS hires FROM emp\n\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countFunction.markdown": "### Count\n返回输入值的总数(计数)。\n\n\n```\nCOUNT(expression)\n```\n- 表达式为字段名称、通配符 (*) 或任何数字值。对于 COUNT(*) 或 COUNT(),将考虑所有值,包括 null 或缺失的值。对于 COUNT(),将不考虑 null 值。\n```\nSELECT COUNT(*) AS count FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.divideOperator.markdown": "### Divide (/)\n```\nSELECT 6 / 3 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.equality.markdown": "### Equality (=)\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no = 10000 LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.firstFunction.markdown": "### FIRST / FIRST_VALUE\n返回按 ordering_field_name 列排序的 field_name 输入列中的第一个非 null 值(如果存在)。如果未提供 ordering_field_name,则仅 field_name 列用于排序。\n\n```\nFIRST(\n field_name \n [, ordering_field_name])\n```\n- 字段名称:用于聚合的目标字段\n- ordering_field_name:用于排序的可选字段。\n\n```\nSELECT gender, FIRST(first_name, birth_date) FROM emp GROUP BY gender ORDER BY gender\n```\n\n- FIRST 不能用在 HAVING 子句中。\n- FIRST 不能用于文本类型的列,除非也将该字段另存为关键字。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.inequality.markdown": "### Inequality (<> or !=)\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no <> 10000 ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.inOperator.markdown": "### IN (, , ...)\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no IN (10000, 10001, 10002, 999) ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.kurtosisFunction.markdown": "### KURTOSIS\n量化字段 field_name 中输入值的分布形状。\n\n```\nKURTOSIS(field_name) \n```\n- 数字字段。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, KURTOSIS(salary) AS k FROM emp\n```\n\n- KURTOSIS 不能用于标量函数或运算符上面,而只能直接用于字段。 \n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.lastFunction.markdown": "### LAST / LAST_VALUE\n这是 FIRST/FIRST_VALUE 的反向函数。返回按 ordering_field_name 列降序排序的 field_name 输入列中的最后一个非 null 值(如果存在)。如果未提供 ordering_field_name,则仅 field_name 列用于排序。 \n\n```\nLAST(\n field_name \n [, ordering_field_name])\n```\n- 字段名称:用于聚合的目标字段\n- ordering_field_name:用于排序的可选字段。\n```\nSELECT gender, LAST(first_name) FROM emp GROUP BY gender ORDER BY gender\n```\n- LAST 不能用在 HAVING 子句中。\n- LAST 不能用于文本类型的列,除非也将该字段另存为关键字。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.madFunction.markdown": "### MAD\n衡量字段 field_name 中输入值的可变性。\n\n```\nMAD(field_name) \n```\n- 数字字段。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, AVG(salary) AS avg, MAD(salary) AS mad FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.markdown": "## 关于 Elasticsearch SQL\n\n使用 Elasticsearch SQL 在 Elasticsearch 内部搜索并聚合数据。此查询语言通过熟悉的语法提供了全文本搜索。这里提供了一个查询示例:\n \n```\nSELECT * FROM library \nORDER BY page_count DESC LIMIT 5\n```\n \nElasticsearch SQL:\n\n- 提供了一组全面的内置运算符和函数。\n- 遵循 SQL 术语和约定。\n- 每行接受一个命令。命令指通过结束输入流终止的一连串令牌\n \n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.maxFunction.markdown": "### MAX\n返回字段 field_name 中所有输入值的最大值。\n\n```\nMAX(field_name) \n```\n- 数字字段。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n\n```\nSELECT MAX(salary) AS max FROM emp\n```\n\n- 类型为文本或关键字的字段的 MAX 将转换为 FIRST/FIRST_VALUE,因此不能用在 HAVING 子句中。\n\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.minFunction.markdown": "### MIN\n返回字段 field_name 中所有输入值的最小值。\n\n```\nMIN(field_name) \n```\n- 数字字段。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n\n```\nSELECT MIN(salary) AS min FROM emp\n```\n\n- 类型为文本或关键字的字段的 MIN 将转换为 FIRST/FIRST_VALUE,因此不能用在 HAVING 子句中。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.moduloOperator.markdown": "### Modulo or remainder(%)\n```\nSELECT 5 % 2 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.multiplyOperator.markdown": "### Multiply (*)\n```\nSELECT 2 * 3 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.negateOperator.markdown": "### Negate (unary -)\n```\nSELECT - 1 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.notOperator.markdown": "### NOT\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE NOT emp_no = 10000 LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.nullNotNull.markdown": "### IS NULL/IS NOT NULL\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no IS NOT NULL AND gender IS NULL\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.nullSafeEquality.markdown": "### Null safe equality:\n```\nSELECT 'elastic' <=> null AS \"equals\"\n\n 等于\n---------------\nfalse\n```\n```\nSELECT null <=> null AS \"equals\"\n\n 等于\n---------------\ntrue\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.orOperator.markdown": "### OR\n```\nSELECT last_name l FROM \"test_emp\" \nWHERE emp_no < 10003 OR emp_no = 10005 ORDER BY emp_no LIMIT 5\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.percentileFunction.markdown": "### PERCENTILE\n返回字段 field_name 中输入值的第 n 个百分位(用 numeric_exp 参数表示)。\n\n```\nPERCENTILE(\n field_name, \n percentile[, \n method[, \n method_parameter]])\n```\n- field_name:数字字段。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n- 百分位数:数字表达式(必须为常数并且不得基于字段)。如果为 null,该函数将返回 null。\n- 方法:百分位数算法的可选字符串文本。可能的值:tdigest 或 hdr。默认值为 tdigest。\n- method_parameter:配置百分位数算法的可选数字文本。为 tdigest 配置表达式,或为 hdr 配置 number_of_significant_value_digits。默认值与后备算法的默认值相同。\n\n```\n精选\n languages,\n PERCENTILE(salary, 97.3, 'tdigest', 100.0) AS \"97.3_TDigest\",\n PERCENTILE(salary, 97.3, 'hdr', 3) AS \"97.3_HDR\"\nFROM emp\nGROUP BY languages\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.percentileRankFunction.markdown": "### PERCENTILE_RANK\n返回字段 field_name 中输入值的第 n 个百分位等级(用 numeric_exp 参数表示)。\n\n```\nPERCENTILE_RANK(\n field_name, \n value[, \n method[, \n method_parameter]]) \n```\n- field_name:数字字段。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n- 百分位数:数字表达式(必须为常数并且不得基于字段)。如果为 null,该函数将返回 null。\n- 方法:百分位数算法的可选字符串文本。可能的值:tdigest 或 hdr。默认值为 tdigest。\n- method_parameter:配置百分位数算法的可选数字文本。为 tdigest 配置表达式,或为 hdr 配置 number_of_significant_value_digits。默认值与后备算法的默认值相同。\n\n```\n精选\n languages,\n ROUND(PERCENTILE_RANK(salary, 65000, 'tdigest', 100.0), 2) AS \"rank_TDigest\",\n ROUND(PERCENTILE_RANK(salary, 65000, 'hdr', 3), 2) AS \"rank_HDR\"\nFROM emp\nGROUP BY languages\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.skewnessFunction.markdown": "### SKEWNESS\n量化字段 field_name 中输入值的非对称分布。\n\n```\nSKEWNESS(field_name) \n```\n- field_name:数字字段。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, SKEWNESS(salary) AS s FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.stsdevpopFunction.markdown": "### STDDEV_POP\n返回字段 field_name 中输入值的填充标准偏差。\n\n```\nSTDDEV_POP(field_name) \n```\n- field_name:数字字段。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, STDDEV_POP(salary) AS stddev FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.stsdevsampFunction.markdown": "### STDDEV_SAMP\n返回字段 field_name 中输入值的样例标准偏差。\n\n```\nSTDDEV_SAMP(field_name) \n```\n- field_name:数字字段。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, STDDEV_SAMP(salary) AS stddev FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.subtractOperator.markdown": "### Subtract (infix -)\n```\nSELECT 1 - 1 AS x\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.sumFunction.markdown": "### SUM\n返回字段 field_name 中所有输入值的总和。\n\n```\nSUM(field_name) \n```\n- 数字字段。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n\n```\nSELECT SUM(salary) AS sum FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.sumofsquaresFunction.markdown": "### SUM_OF_SQUARES\n返回字段 field_name 中输入值的平方和。\n\n```\nSUM_OF_SQUARES(field_name) \n```\n- field_name:数字字段。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, SUM_OF_SQUARES(salary) AS sumsq\n FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.varpopFunction.markdown": "### VAR_POP\n返回字段 field_name 中输入值的总体方差。\n\n```\nVAR_POP(field_name) \n```\n- field_name:数字字段。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, VAR_POP(salary) AS varpop FROM emp\n```\n ", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.varsampFunction.markdown": "### VAR_SAMP\n返回字段 field_name 中输入值的样例方差。\n\n```\nVAR_SAMP(field_name) \n```\n- field_name:数字字段。如果此字段仅包含 null 值,此函数将返回 null。否则,该函数将忽略此字段中的 null 值。\n\n```\nSELECT MIN(salary) AS min, MAX(salary) AS max, VAR_SAMP(salary) AS varsamp FROM emp\n```\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.absFunction.markdown": "### ABS\n返回绝对值。\n\n```\nFROM employees\n| KEEP first_name, last_name, height\n| EVAL abs_height = ABS(0.0 - height)\n```\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.acosFunction.markdown": "### ACOS\n反余弦三角函数。\n\n```\nROW a=.9\n| EVAL acos=ACOS(a)\n```\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.asinFunction.markdown": "### ASIN\n反正弦三角函数。\n\n```\nROW a=.9\n| EVAL asin=ASIN(a)\n```\n ", @@ -5700,49 +5664,10 @@ "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.toVersionFunction.markdown": "### TO_VERSION\n将输入字符串转换为版本值。例如:\n\n```\nROW v = TO_VERSION(\"1.2.3\")\n```\n\n返回:\n\n```\n1.2.3\n```\n\n别名:TO_VER\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.trimFunction.markdown": "### TRIM\n从字符串中移除前导和尾随空格。\n\n```\nROW message = \" some text \", color = \" red \"\n| EVAL message = TRIM(message)\n| EVAL color = TRIM(color)\n```\n ", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.where.markdown": "### WHERE\n使用 `WHERE` 可生成一个表,其中包含输入表中所提供的条件评估为 `true` 的所有行:\n \n```\nFROM employees\n| KEEP first_name, last_name, still_hired\n| WHERE still_hired == true\n```\n\n#### 运算符\n\n请参阅**运算符**了解所支持的运算符的概览。\n\n#### 函数\n`WHERE` 支持各种用于计算值的函数。请参阅**函数**了解更多信息。\n ", - "textBasedEditor.query.textBasedLanguagesEditor.aggregateFunctions": "聚合函数", - "textBasedEditor.query.textBasedLanguagesEditor.aggregateFunctionsDocumentationDescription": "用于从一组输入值计算单一结果的函数。Elasticsearch SQL 仅在与分组(隐式或显式)一起时才支持聚合函数。", "textBasedEditor.query.textBasedLanguagesEditor.aggregationFunctions": "聚合函数", "textBasedEditor.query.textBasedLanguagesEditor.aggregationFunctionsDocumentationESQLDescription": "这些函数可以与 STATS...BY 搭配使用:", "textBasedEditor.query.textBasedLanguagesEditor.commandsDescription": "源命令会生成一个表,其中通常包含来自 Elasticsearch 的数据。ES|QL 支持以下源命令。", - "textBasedEditor.query.textBasedLanguagesEditor.comparisonOperators": "比较运算符", - "textBasedEditor.query.textBasedLanguagesEditor.comparisonOperatorsDocumentationDescription": "用于比较一个或多个表达式的布尔运算符。", "textBasedEditor.query.textBasedLanguagesEditor.disableWordWrapLabel": "禁止使用管道符换行", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.AddOperator": "添加", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.AndOperator": "且", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.averageFunction": "平均值", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.between": "介于", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.comparison": "对比", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countAllFunction": "Count (All)", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countDistinctFunction": "Count (Distinct)", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.countFunction": "计数", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.divideOperator": "除", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.equality": "等于", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.firstFunction": "First / First_value", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.inequality": "不等于", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.inOperator": "IN", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.kurtosisFunction": "峰度", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.lastFunction": "Last / Last_value", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.madFunction": "Mad", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.maxFunction": "最大值", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.minFunction": "最小值", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.moduloOperator": "模数或余数", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.multiplyOperator": "乘积", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.negateOperator": "求反", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.NotOperator": "非", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.nullNotNull": "IS NULL 和 IS NOT NULL", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.nullSafeEquality": "Null safe equality (<=>)", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.OrOperator": "OR", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.percentileFunction": "百分位数", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.percentileRankFunction": "百分位等级", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.skewnessFunction": "偏度", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.stsdevpopFunction": "STDDEV_POP", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.stsdevsampFunction": "STDDEV_SAMP", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.SubtractOperator": "减", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.sumFunction": "求和", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.sumofsquaresFunction": "平方和", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.varpopFunction": "VAR_POP", - "textBasedEditor.query.textBasedLanguagesEditor.documentation.varsampFunction": "VAR_SAMP", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.absFunction": "ABS", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.acosFunction": "ACOS", "textBasedEditor.query.textBasedLanguagesEditor.documentationESQL.asinFunction": "ASIN", @@ -5839,11 +5764,6 @@ "textBasedEditor.query.textBasedLanguagesEditor.expandTooltip": "展开查询编辑器", "textBasedEditor.query.textBasedLanguagesEditor.functions": "函数", "textBasedEditor.query.textBasedLanguagesEditor.functionsDocumentationESQLDescription": "ROW、EVAL 和 WHERE 支持的函数。", - "textBasedEditor.query.textBasedLanguagesEditor.howItWorks": "运作方式", - "textBasedEditor.query.textBasedLanguagesEditor.logicalOperators": "逻辑运算符", - "textBasedEditor.query.textBasedLanguagesEditor.logicalOperatorsDocumentationDescription": "用于评估一个或多个表达式的布尔运算符。", - "textBasedEditor.query.textBasedLanguagesEditor.mathOperators": "数学运算符", - "textBasedEditor.query.textBasedLanguagesEditor.mathOperatorsDocumentationDescription": "执行影响一个或两个值的数学运算。结果为数值类型的值。", "textBasedEditor.query.textBasedLanguagesEditor.MinimizeEditor": "最小化编辑器", "textBasedEditor.query.textBasedLanguagesEditor.minimizeTooltip": "压缩查询编辑器", "textBasedEditor.query.textBasedLanguagesEditor.operators": "运算符", @@ -34384,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": "显示所有正在运行的进程", @@ -35544,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();