diff --git a/.buildkite/scripts/steps/check_types_commits.sh b/.buildkite/scripts/steps/check_types_commits.sh
index d34c4dae5ffa9..973e4ce6fbb60 100755
--- a/.buildkite/scripts/steps/check_types_commits.sh
+++ b/.buildkite/scripts/steps/check_types_commits.sh
@@ -2,40 +2,92 @@
set -euo pipefail
-# This script will collect typescript projects and run typecheck on projects between the given 2 parameters
-# Could be used for selective typechecking on projects that might be affected for a given PR.
+# This script detects the files changed in a given set of commits, finds the related tsconfig.json files, and scope the TypeScript type check to those.
+# In CI, this script can be used for selective type-checking on projects that might be affected for a given PR.
# (The accuracy for finding related projects is not a 100%)
-if [[ "${CI-}" == "true" ]]; then
- .buildkite/scripts/bootstrap.sh
+argv=( "$@" )
+diffArgs=("--name-only")
+uniq_dirs=()
+uniq_tsconfigs=()
- sha1=$(git merge-base $GITHUB_PR_TARGET_BRANCH $GITHUB_PR_TRIGGERED_SHA)
- sha2="${GITHUB_PR_TRIGGERED_SHA-}"
-else
- if [[ "${1-}" == "--cached" ]]; then
- # Only check staged files
- sha1=$1
- sha2=""
+is_flag_set () {
+ flag=$1
+ if [ ${#argv[@]} -gt 0 ] && [[ ${argv[@]} =~ $flag ]]; then
+ true
else
- # Script take between 0 and 2 arguments representing two commit SHA's:
- # If 0, it will diff HEAD and HEAD^
- # If 1 (SHA1), it will diff SHA1 and SHA1^
- # If 2 (SHA1, SHA2), it will diff SHA1 and SHA2
- sha1="${1-HEAD}"
- sha2="${2-$sha1^}"
+ false
fi
+}
+
+get_args_for_flag_result=()
+get_args_for_flag () {
+ flag=$1
+ found=false
+ get_args_for_flag_result=()
+ if [ ${#argv[@]} -gt 0 ]; then
+ for i in "${!argv[@]}"; do
+ arg="${argv[$i]}"
+ if [ "$found" == false ] && [[ "$arg" == "$flag" ]]; then
+ found=true
+ elif [ "$found" == true ]; then
+ if [[ "$arg" == -* ]]; then
+ return
+ else
+ get_args_for_flag_result+=("$arg")
+ fi
+ fi
+ done
+ fi
+}
+
+if is_flag_set "--help" || is_flag_set "-h"; then
+ echo "Detects the files changed in a given set of commits, finds the related"
+ echo "tsconfig.json files, and scope the TypeScript type check to those."
+ echo
+ echo "Usage:"
+ echo " $0 [options]"
+ echo " $0 [ []]"
+ echo
+ echo "Options:"
+ echo " --help, -h Show this help"
+ echo " --cached Check staged changes"
+ echo " --merge-base [ []]"
+ echo " Check changes between nearest common ansestor (merge-base) of"
+ echo " ref1 and ref2. Defaults: 'main' and 'HEAD'"
+ echo
+ echo "If no options are provided, the script takes between 0 and 2 arguments"
+ echo "representing two git refs:"
+ echo " If 0, it will diff HEAD and HEAD^"
+ echo " If 1 (REF1), it will diff REF1 and REF1^"
+ echo " If 2 (REF1, REF2), it will diff REF1 and REF2"
+ exit
fi
-uniq_dirs=()
-uniq_tsconfigs=()
+if [[ "${CI-}" == "true" ]]; then
+ # Buildkite only
+ .buildkite/scripts/bootstrap.sh
-if [[ "$sha1" == "--cached" ]]; then
- echo "Detecting files changed in staging area..."
+ targetBranch="${GITHUB_PR_TARGET_BRANCH-}"
+ git fetch origin $targetBranch
+ sha=$(git merge-base "origin/$targetBranch" "${GITHUB_PR_TRIGGERED_SHA-}")
+ diffArgs+=("$sha" "${GITHUB_PR_TRIGGERED_SHA-}")
+elif is_flag_set "--merge-base"; then
+ # Similar to when CI=true, but locally
+ get_args_for_flag "--merge-base"
+ diffArgs+=("--merge-base" "${get_args_for_flag_result[0]-main}" "${get_args_for_flag_result[1]-HEAD}")
+elif is_flag_set "--cached"; then
+ # Only check staged files
+ diffArgs+=("--cached")
else
- echo "Detecting files changed between $sha1 and $sha2..."
+ # Full manual mode!
+ ref1="${1-HEAD}"
+ diffArgs+=("$ref1" "${2-$ref1^}")
fi
-files=($(git diff --name-only $sha1 $sha2))
+echo "Detecting files changed..."
+echo "DEBUG: git diff args: ${diffArgs[@]}"
+files=($(git diff "${diffArgs[@]}"))
add_dir () {
new_dir=$1
@@ -117,11 +169,7 @@ if [ ${#uniq_dirs[@]} -gt 0 ]; then
fi
if [ ${#uniq_tsconfigs[@]} -eq 0 ]; then
- if [[ "$sha1" == "--cached" ]]; then
- echo "No tsconfig.json files found for staged changes"
- else
- echo "No tsconfig.json files found for changes between $sha1 and $sha2"
- fi
+ echo "No tsconfig.json files found"
exit
fi
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index dd7ceab0cbd5c..31bcc2356bb28 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -274,6 +274,7 @@ packages/core/status/core-status-server-mocks @elastic/kibana-core
packages/core/test-helpers/core-test-helpers-deprecations-getters @elastic/kibana-core
packages/core/test-helpers/core-test-helpers-http-setup-browser @elastic/kibana-core
packages/core/test-helpers/core-test-helpers-kbn-server @elastic/kibana-core
+packages/core/test-helpers/core-test-helpers-model-versions @elastic/kibana-core
packages/core/test-helpers/core-test-helpers-so-type-serializer @elastic/kibana-core
packages/core/test-helpers/core-test-helpers-test-utils @elastic/kibana-core
packages/core/theme/core-theme-browser @elastic/kibana-core
@@ -1239,6 +1240,7 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib
/x-pack/plugins/security_solution/public/common/components/ml_popover @elastic/security-detection-rule-management
/x-pack/plugins/security_solution/public/common/components/popover_items @elastic/security-detection-rule-management
/x-pack/plugins/security_solution/public/detection_engine/fleet_integrations @elastic/security-detection-rule-management
+/x-pack/plugins/security_solution/public/detection_engine/endpoint_exceptions @elastic/security-defend-workflows
/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui @elastic/security-detection-rule-management
/x-pack/plugins/security_solution/public/detection_engine/rule_management @elastic/security-detection-rule-management
/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui @elastic/security-detection-rule-management
@@ -1336,6 +1338,7 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib
/x-pack/test/security_solution_endpoint_api_int/ @elastic/security-defend-workflows
/x-pack/test_serverless/shared/lib/security/kibana_roles/ @elastic/security-defend-workflows
/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management @elastic/security-defend-workflows
+/x-pack/plugins/security_solution_serverless/public/upselling/pages/endpoint_management @elastic/security-defend-workflows
/x-pack/plugins/security_solution_serverless/server/endpoint @elastic/security-defend-workflows
## Security Solution sub teams - security-telemetry (Data Engineering)
diff --git a/package.json b/package.json
index b099df2f57086..e95c9000155a9 100644
--- a/package.json
+++ b/package.json
@@ -936,8 +936,8 @@
"jsts": "^1.6.2",
"kea": "^2.4.2",
"langchain": "^0.0.151",
- "launchdarkly-js-client-sdk": "^2.22.1",
- "launchdarkly-node-server-sdk": "^6.4.2",
+ "launchdarkly-js-client-sdk": "^3.1.4",
+ "launchdarkly-node-server-sdk": "^7.0.3",
"load-json-file": "^6.2.0",
"lodash": "^4.17.21",
"lru-cache": "^4.1.5",
@@ -1180,6 +1180,7 @@
"@kbn/core-saved-objects-server-mocks": "link:packages/core/saved-objects/core-saved-objects-server-mocks",
"@kbn/core-status-server-mocks": "link:packages/core/status/core-status-server-mocks",
"@kbn/core-test-helpers-kbn-server": "link:packages/core/test-helpers/core-test-helpers-kbn-server",
+ "@kbn/core-test-helpers-model-versions": "link:packages/core/test-helpers/core-test-helpers-model-versions",
"@kbn/core-theme-browser-mocks": "link:packages/core/theme/core-theme-browser-mocks",
"@kbn/core-ui-settings-browser-mocks": "link:packages/core/ui-settings/core-ui-settings-browser-mocks",
"@kbn/core-ui-settings-server-mocks": "link:packages/core/ui-settings/core-ui-settings-server-mocks",
diff --git a/packages/core/test-helpers/core-test-helpers-model-versions/README.md b/packages/core/test-helpers/core-test-helpers-model-versions/README.md
new file mode 100644
index 0000000000000..71aca755a14b6
--- /dev/null
+++ b/packages/core/test-helpers/core-test-helpers-model-versions/README.md
@@ -0,0 +1,19 @@
+# @kbn/core-test-helpers-model-versions
+
+Package exposing utilities for model version integration testing.
+
+This package exposes a `createModelVersionTestBed` utility which allow simulating
+a testbed environment where we're in the cohabitation period between two versions, to test the interactions
+between two model versions of a set of SO types.
+
+### Limitations:
+
+Because the test bed is only creating the parts of Core required to create the two SO
+repositories, and because we're not loading all plugins (for proper isolation), the integration
+test bed has some limitations:
+
+- no extensions are enabled
+ - no security
+ - no encryption
+ - no spaces
+- all SO types will be using the same SO index
\ No newline at end of file
diff --git a/packages/core/test-helpers/core-test-helpers-model-versions/index.ts b/packages/core/test-helpers/core-test-helpers-model-versions/index.ts
new file mode 100644
index 0000000000000..e8a5565f1dd79
--- /dev/null
+++ b/packages/core/test-helpers/core-test-helpers-model-versions/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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.
+ */
+
+export { createModelVersionTestBed } from './src/test_bed';
+
+export type {
+ ModelVersionTestBed,
+ ModelVersionTestKit,
+ ModelVersionTestkitOptions,
+ SavedObjectTestkitDefinition,
+} from './src/types';
diff --git a/packages/core/test-helpers/core-test-helpers-model-versions/jest.config.js b/packages/core/test-helpers/core-test-helpers-model-versions/jest.config.js
new file mode 100644
index 0000000000000..77b74018b2b85
--- /dev/null
+++ b/packages/core/test-helpers/core-test-helpers-model-versions/jest.config.js
@@ -0,0 +1,13 @@
+/*
+ * 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.
+ */
+
+module.exports = {
+ preset: '@kbn/test/jest_node',
+ rootDir: '../../../..',
+ roots: ['/packages/core/test-helpers/core-test-helpers-model-versions'],
+};
diff --git a/packages/core/test-helpers/core-test-helpers-model-versions/kibana.jsonc b/packages/core/test-helpers/core-test-helpers-model-versions/kibana.jsonc
new file mode 100644
index 0000000000000..d6ea333ad06f7
--- /dev/null
+++ b/packages/core/test-helpers/core-test-helpers-model-versions/kibana.jsonc
@@ -0,0 +1,6 @@
+{
+ "type": "shared-common",
+ "id": "@kbn/core-test-helpers-model-versions",
+ "owner": "@elastic/kibana-core",
+ "devOnly": true
+}
diff --git a/packages/core/test-helpers/core-test-helpers-model-versions/package.json b/packages/core/test-helpers/core-test-helpers-model-versions/package.json
new file mode 100644
index 0000000000000..13e5760f1a1d5
--- /dev/null
+++ b/packages/core/test-helpers/core-test-helpers-model-versions/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@kbn/core-test-helpers-model-versions",
+ "private": true,
+ "version": "1.0.0",
+ "license": "SSPL-1.0 OR Elastic License 2.0"
+}
\ No newline at end of file
diff --git a/packages/core/test-helpers/core-test-helpers-model-versions/src/elasticsearch.ts b/packages/core/test-helpers/core-test-helpers-model-versions/src/elasticsearch.ts
new file mode 100644
index 0000000000000..3af641d5597aa
--- /dev/null
+++ b/packages/core/test-helpers/core-test-helpers-model-versions/src/elasticsearch.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { createTestServers, type TestElasticsearchUtils } from '@kbn/core-test-helpers-kbn-server';
+
+/**
+ * Start the traditional ES cluster and return the instance.
+ */
+export const startElasticsearch = async ({
+ basePath,
+ dataArchive,
+ timeout,
+}: {
+ basePath?: string;
+ dataArchive?: string;
+ timeout?: number;
+} = {}): Promise => {
+ const { startES } = createTestServers({
+ adjustTimeout: (t: number) => jest.setTimeout(t + (timeout ?? 0)),
+ settings: {
+ es: {
+ license: 'basic',
+ basePath,
+ dataArchive,
+ },
+ },
+ });
+ return await startES();
+};
diff --git a/packages/core/test-helpers/core-test-helpers-model-versions/src/test_bed.ts b/packages/core/test-helpers/core-test-helpers-model-versions/src/test_bed.ts
new file mode 100644
index 0000000000000..aa8c4ab8df425
--- /dev/null
+++ b/packages/core/test-helpers/core-test-helpers-model-versions/src/test_bed.ts
@@ -0,0 +1,89 @@
+/*
+ * 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 { TestElasticsearchUtils } from '@kbn/core-test-helpers-kbn-server';
+import { startElasticsearch } from './elasticsearch';
+import { prepareModelVersionTestKit } from './test_kit';
+import type { ModelVersionTestBed } from './types';
+
+/**
+ * Create a {@link ModelVersionTestBed} that can be used for model version integration testing.
+ *
+ * @example
+ * ```ts
+ * describe('myIntegrationTest', () => {
+ * const testbed = createModelVersionTestBed();
+ * let testkit: ModelVersionTestKit;
+ *
+ * beforeAll(async () => {
+ * await testbed.startES();
+ * });
+ *
+ * afterAll(async () => {
+ * await testbed.stopES();
+ * });
+ *
+ * beforeEach(async () => {
+ * testkit = await testbed.prepareTestKit({
+ * savedObjectDefinitions: [{
+ * definition: mySoTypeDefinition,
+ * modelVersionBefore: 1,
+ * modelVersionAfter: 2,
+ * }]
+ * })
+ * });
+ *
+ * afterEach(async () => {
+ * if(testkit) {
+ * await testkit.tearsDown();
+ * }
+ * });
+ *
+ * it('can be used to test model version cohabitation', async () => {
+ * // last registered version is `1`
+ * const repositoryV1 = testkit.repositoryBefore;
+ * // last registered version is `2`
+ * const repositoryV2 = testkit.repositoryAfter;
+ *
+ * // do something with the two repositories, e.g
+ * await repositoryV1.create(someAttrs, { id });
+ * const v2docReadFromV1 = await repositoryV2.get('my-type', id);
+ * expect(v2docReadFromV1.attributes).toEqual(something);
+ * })
+ * })
+ * ```
+ *
+ * @public
+ */
+export const createModelVersionTestBed = (): ModelVersionTestBed => {
+ let elasticsearch: TestElasticsearchUtils | undefined;
+
+ const startES = async () => {
+ if (elasticsearch) {
+ throw new Error('Elasticsearch already started');
+ }
+ elasticsearch = await startElasticsearch();
+ };
+
+ const stopES = async () => {
+ if (!elasticsearch) {
+ throw new Error('Elasticsearch not started');
+ }
+ await elasticsearch.stop();
+ await delay(10);
+ elasticsearch = undefined;
+ };
+
+ return {
+ startES,
+ stopES,
+ prepareTestKit: prepareModelVersionTestKit,
+ };
+};
+
+const delay = (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
diff --git a/packages/core/test-helpers/core-test-helpers-model-versions/src/test_kit.ts b/packages/core/test-helpers/core-test-helpers-model-versions/src/test_kit.ts
new file mode 100644
index 0000000000000..93517ea3b33a6
--- /dev/null
+++ b/packages/core/test-helpers/core-test-helpers-model-versions/src/test_kit.ts
@@ -0,0 +1,264 @@
+/*
+ * 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 fs from 'fs/promises';
+import { defaultsDeep } from 'lodash';
+import { BehaviorSubject, firstValueFrom, map } from 'rxjs';
+import { ConfigService, Env } from '@kbn/config';
+import { getEnvOptions } from '@kbn/config-mocks';
+import { REPO_ROOT } from '@kbn/repo-info';
+import { KibanaMigrator } from '@kbn/core-saved-objects-migration-server-internal';
+import {
+ SavedObjectConfig,
+ type SavedObjectsConfigType,
+ type SavedObjectsMigrationConfigType,
+ type IndexTypesMap,
+} from '@kbn/core-saved-objects-base-server-internal';
+import { SavedObjectsRepository } from '@kbn/core-saved-objects-api-server-internal';
+import {
+ ElasticsearchConfig,
+ type ElasticsearchConfigType,
+ getCapabilitiesFromClient,
+} from '@kbn/core-elasticsearch-server-internal';
+import { AgentManager, configureClient } from '@kbn/core-elasticsearch-client-server-internal';
+import { type LoggingConfigType, LoggingSystem } from '@kbn/core-logging-server-internal';
+import { ISavedObjectTypeRegistry } from '@kbn/core-saved-objects-server';
+import { esTestConfig, kibanaServerTestUser } from '@kbn/test';
+import type { LoggerFactory } from '@kbn/logging';
+import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
+import { registerServiceConfig } from '@kbn/core-root-server-internal';
+import { getDocLinks, getDocLinksMeta } from '@kbn/doc-links';
+import type { DocLinksServiceStart } from '@kbn/core-doc-links-server';
+import type { NodeRoles } from '@kbn/core-node-server';
+import { getTypeRegistries } from './type_registry';
+import type { ModelVersionTestkitOptions, ModelVersionTestKit } from './types';
+
+const env = Env.createDefault(REPO_ROOT, getEnvOptions());
+const currentVersion = env.packageInfo.version;
+const currentBranch = env.packageInfo.branch;
+const defaultKibanaIndex = '.kibana_migrator_tests';
+const defaultNodeRoles: NodeRoles = { migrator: true, ui: true, backgroundTasks: true };
+
+/**
+ * Prepare the model version integration test kit
+ *
+ * @internal
+ */
+export const prepareModelVersionTestKit = async ({
+ savedObjectDefinitions,
+ objectsToCreateBetween = [],
+ settingOverrides = {},
+ kibanaBranch = currentBranch,
+ kibanaVersion = currentVersion,
+ kibanaIndex = defaultKibanaIndex,
+ logFilePath,
+}: ModelVersionTestkitOptions): Promise => {
+ await fs.unlink(logFilePath).catch(() => {});
+
+ const loggingSystem = new LoggingSystem();
+ const loggerFactory = loggingSystem.asLoggerFactory();
+
+ const configService = getConfigService(settingOverrides, loggerFactory, logFilePath);
+
+ // configure logging system
+ const loggingConf = await firstValueFrom(configService.atPath('logging'));
+ await loggingSystem.upgrade(loggingConf);
+
+ const esClient = await getElasticsearchClient(configService, loggerFactory, kibanaVersion);
+
+ const { registryBefore, registryAfter } = getTypeRegistries({
+ types: savedObjectDefinitions,
+ kibanaIndex,
+ });
+
+ const commonMigratorParams = {
+ configService,
+ client: esClient,
+ loggerFactory,
+ kibanaIndex,
+ defaultIndexTypesMap: {},
+ kibanaVersion,
+ kibanaBranch,
+ nodeRoles: defaultNodeRoles,
+ };
+
+ const firstMigrator = await getMigrator({
+ ...commonMigratorParams,
+ typeRegistry: registryBefore,
+ });
+
+ const secondMigrator = await getMigrator({
+ ...commonMigratorParams,
+ typeRegistry: registryAfter,
+ });
+
+ const repositoryBefore = SavedObjectsRepository.createRepository(
+ firstMigrator,
+ registryBefore,
+ kibanaIndex,
+ esClient,
+ loggerFactory.get('saved_objects')
+ );
+
+ const repositoryAfter = SavedObjectsRepository.createRepository(
+ secondMigrator,
+ registryAfter,
+ kibanaIndex,
+ esClient,
+ loggerFactory.get('saved_objects')
+ );
+
+ await runMigrations(firstMigrator);
+
+ if (objectsToCreateBetween.length) {
+ await repositoryBefore.bulkCreate(objectsToCreateBetween, { refresh: 'wait_for' });
+ }
+
+ await runMigrations(secondMigrator);
+
+ const tearsDown = async () => {
+ await esClient.indices.delete({ index: `${kibanaIndex}_*`, allow_no_indices: true });
+ };
+
+ return {
+ esClient,
+ repositoryBefore,
+ repositoryAfter,
+ tearsDown,
+ };
+};
+
+const getConfigService = (
+ settings: Record,
+ loggerFactory: LoggerFactory,
+ logFilePath: string
+) => {
+ // Define some basic default kibana settings
+ const DEFAULTS_SETTINGS = {
+ server: {
+ autoListen: true,
+ // Use the ephemeral port to make sure that tests use the first available
+ // port and aren't affected by the timing issues in test environment.
+ port: 0,
+ xsrf: { disableProtection: true },
+ },
+ elasticsearch: {
+ hosts: [esTestConfig.getUrl()],
+ username: kibanaServerTestUser.username,
+ password: kibanaServerTestUser.password,
+ },
+ migrations: {
+ algorithm: 'v2',
+ skip: false,
+ },
+ logging: {
+ appenders: {
+ file: {
+ type: 'file',
+ fileName: logFilePath,
+ layout: {
+ type: 'json',
+ },
+ },
+ },
+ loggers: [
+ {
+ name: 'root',
+ level: 'info',
+ appenders: ['file'],
+ },
+ ],
+ },
+ plugins: {},
+ };
+
+ const rawConfigProvider = {
+ getConfig$: () => new BehaviorSubject(defaultsDeep({}, settings, DEFAULTS_SETTINGS)),
+ };
+
+ const configService = new ConfigService(rawConfigProvider, env, loggerFactory);
+ registerServiceConfig(configService);
+ return configService;
+};
+
+const getElasticsearchClient = async (
+ configService: ConfigService,
+ loggerFactory: LoggerFactory,
+ kibanaVersion: string
+) => {
+ const esClientConfig = await firstValueFrom(
+ configService
+ .atPath('elasticsearch')
+ .pipe(map((rawConfig) => new ElasticsearchConfig(rawConfig)))
+ );
+
+ return configureClient(esClientConfig, {
+ logger: loggerFactory.get('elasticsearch'),
+ type: 'data',
+ agentFactoryProvider: new AgentManager(
+ loggerFactory.get('elasticsearch-service', 'agent-manager')
+ ),
+ kibanaVersion,
+ });
+};
+
+const getMigrator = async ({
+ configService,
+ client,
+ kibanaIndex,
+ typeRegistry,
+ defaultIndexTypesMap,
+ loggerFactory,
+ kibanaVersion,
+ kibanaBranch,
+ nodeRoles,
+}: {
+ configService: ConfigService;
+ client: ElasticsearchClient;
+ kibanaIndex: string;
+ typeRegistry: ISavedObjectTypeRegistry;
+ defaultIndexTypesMap: IndexTypesMap;
+ loggerFactory: LoggerFactory;
+ kibanaVersion: string;
+ kibanaBranch: string;
+ nodeRoles: NodeRoles;
+}) => {
+ const savedObjectsConf = await firstValueFrom(
+ configService.atPath('savedObjects')
+ );
+ const savedObjectsMigrationConf = await firstValueFrom(
+ configService.atPath('migrations')
+ );
+ const soConfig = new SavedObjectConfig(savedObjectsConf, savedObjectsMigrationConf);
+
+ const docLinks: DocLinksServiceStart = {
+ ...getDocLinksMeta({ kibanaBranch }),
+ links: getDocLinks({ kibanaBranch }),
+ };
+
+ const esCapabilities = await getCapabilitiesFromClient(client);
+
+ return new KibanaMigrator({
+ client,
+ kibanaIndex,
+ typeRegistry,
+ defaultIndexTypesMap,
+ soMigrationsConfig: soConfig.migration,
+ kibanaVersion,
+ logger: loggerFactory.get('savedobjects-service'),
+ docLinks,
+ waitForMigrationCompletion: false, // ensure we have an active role in the migration
+ nodeRoles,
+ esCapabilities,
+ });
+};
+
+const runMigrations = async (migrator: KibanaMigrator) => {
+ migrator.prepareMigrations();
+ await migrator.runMigrations();
+};
diff --git a/packages/core/test-helpers/core-test-helpers-model-versions/src/type_registry.ts b/packages/core/test-helpers/core-test-helpers-model-versions/src/type_registry.ts
new file mode 100644
index 0000000000000..62751737b594b
--- /dev/null
+++ b/packages/core/test-helpers/core-test-helpers-model-versions/src/type_registry.ts
@@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { SavedObjectsType } from '@kbn/core-saved-objects-server';
+import { SavedObjectTypeRegistry } from '@kbn/core-saved-objects-base-server-internal';
+import type { SavedObjectTestkitDefinition } from './types';
+
+export interface TestkitTypeRegistries {
+ registryBefore: SavedObjectTypeRegistry;
+ registryAfter: SavedObjectTypeRegistry;
+}
+
+/**
+ * Create the 'before' and 'after' type registries from the provided testkit type definitions.
+ */
+export const getTypeRegistries = ({
+ types,
+ kibanaIndex,
+}: {
+ types: SavedObjectTestkitDefinition[];
+ kibanaIndex: string;
+}): TestkitTypeRegistries => {
+ const registryBefore = new SavedObjectTypeRegistry();
+ const registryAfter = new SavedObjectTypeRegistry();
+
+ for (const definition of types) {
+ const { typeBefore, typeAfter } = getTypes({ definition, kibanaIndex });
+ registryBefore.registerType(typeBefore);
+ registryAfter.registerType(typeAfter);
+ }
+
+ return {
+ registryBefore,
+ registryAfter,
+ };
+};
+
+const getTypes = ({
+ definition,
+ kibanaIndex,
+}: {
+ definition: SavedObjectTestkitDefinition;
+ kibanaIndex: string;
+}): { typeBefore: SavedObjectsType; typeAfter: SavedObjectsType } => {
+ const modelVersionMap =
+ typeof definition.definition.modelVersions === 'function'
+ ? definition.definition.modelVersions()
+ : definition.definition.modelVersions ?? {};
+
+ const typeBefore: SavedObjectsType = {
+ ...definition.definition,
+ indexPattern: kibanaIndex,
+ modelVersions: removeKeysGreaterThan(modelVersionMap, definition.modelVersionBefore),
+ };
+
+ const typeAfter: SavedObjectsType = {
+ ...definition.definition,
+ indexPattern: kibanaIndex,
+ modelVersions: removeKeysGreaterThan(modelVersionMap, definition.modelVersionAfter),
+ };
+
+ return {
+ typeBefore,
+ typeAfter,
+ };
+};
+
+const removeKeysGreaterThan = >(
+ record: T,
+ version: number
+): T => {
+ return Object.fromEntries(
+ Object.entries(record).filter(([key, value]) => {
+ return parseInt(key, 10) <= version;
+ })
+ ) as T;
+};
diff --git a/packages/core/test-helpers/core-test-helpers-model-versions/src/types.ts b/packages/core/test-helpers/core-test-helpers-model-versions/src/types.ts
new file mode 100644
index 0000000000000..37f22a297ee31
--- /dev/null
+++ b/packages/core/test-helpers/core-test-helpers-model-versions/src/types.ts
@@ -0,0 +1,147 @@
+/*
+ * 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 { Client } from '@elastic/elasticsearch';
+import type { SavedObjectsType } from '@kbn/core-saved-objects-server';
+import type {
+ ISavedObjectsRepository,
+ SavedObjectsBulkCreateObject,
+} from '@kbn/core-saved-objects-api-server';
+
+/**
+ * A testbed that can be used for model version integration tests.
+ *
+ * @public
+ */
+export interface ModelVersionTestBed {
+ /**
+ * Starts the ES cluster.
+ * This should usually be called only once before the suite runs, within a `beforeAll` block.
+ */
+ startES: () => Promise;
+ /**
+ * Stops the ES cluster.
+ * This should usually be called only after the suite runs, within a `afterAll` block.
+ */
+ stopES: () => Promise;
+ /**
+ * Prepare and return the testkit instance.
+ *
+ * @see {@link ModelVersionTestkitOptions}
+ * @see {@link ModelVersionTestKit}
+ */
+ prepareTestKit: (options: ModelVersionTestkitOptions) => Promise;
+}
+
+/**
+ * Options used to create a {@link ModelVersionTestKit} via {@link ModelVersionTestBed#prepareTestKit}
+ *
+ * @public
+ */
+export interface ModelVersionTestkitOptions {
+ /**
+ * The {@link SavedObjectTestkitDefinition | definitions} for the SO type(s) to test
+ */
+ savedObjectDefinitions: SavedObjectTestkitDefinition[];
+ /**
+ * The path of the file to write logs to.
+ * Necessary because the testkit doesn't know the test's location
+ *
+ * @example
+ * ```ts
+ * const logFilePath = Path.join(__dirname, '{my_test}.log');
+ * ```
+ */
+ logFilePath: string;
+ /**
+ * (optional) if specified, the provided list of objects will be created (using `SOR.bulkCreate`)
+ * between the first (before) the second (after) migrator runs. Objects are therefor expected to be of
+ * the `versionBefore` version.
+ */
+ objectsToCreateBetween?: SavedObjectsBulkCreateObject[];
+ /**
+ * (optional) raw record of settings to be used to override the default Kibana configuration.
+ * if provided, will be merged by the default test configuration.
+ *
+ * @example
+ * ```
+ * const settingOverrides = {
+ * migrations: {
+ * algorithm: 'zdt,
+ * }
+ * }
+ * ```
+ */
+ settingOverrides?: Record;
+ /**
+ * (optional) allows to override the kibanaVersion that will be passed down to the migrator instances
+ * Defaults to the version coming from the package.json.
+ */
+ kibanaVersion?: string;
+ /**
+ * (optional) allows to override the kibanaBranch that will be passed down to the migrator instances
+ * Defaults to the version coming from the package.json.
+ */
+ kibanaBranch?: string;
+ /**
+ * (optional) the index (pattern) to use for all types.
+ * Defaults to `.kibana_migrator_tests`
+ */
+ kibanaIndex?: string;
+}
+
+/**
+ * Testkit composed of various services that can be used to run the
+ * model version integration tests.
+ *
+ * Mostly composed of the two `repositoryBefore` and `repositoryAfter` repositories
+ * that can be used to interact with different versions of the SO types.
+ *
+ * @public
+ */
+export interface ModelVersionTestKit {
+ /**
+ * An ES client connecting to the Elasticsearch cluster used by the testkit.
+ */
+ esClient: Client;
+ /**
+ * The SO repository using the SO type definitions at the `before` versions.
+ */
+ repositoryBefore: ISavedObjectsRepository;
+ /**
+ * The SO repository using the SO type definitions at the `after` versions.
+ */
+ repositoryAfter: ISavedObjectsRepository;
+ /**
+ * Cleanup function that will delete the test index.
+ * Should be called before calling `testbed.prepareTestKit` again.
+ */
+ tearsDown: () => Promise;
+}
+
+/**
+ * Represents the info necessary to prepare a given type for the sandbox.
+ * Contains both the actual SO type definition, and the versions
+ * that should be used at 'before' and 'after' model versions.
+ *
+ * @public
+ */
+export interface SavedObjectTestkitDefinition {
+ /**
+ * The SO type definition
+ */
+ definition: SavedObjectsType;
+ /**
+ * The model version to be used for the 'before' repository.
+ */
+ modelVersionBefore: number;
+ /**
+ * The model version to be used for the 'after' repository.
+ */
+ modelVersionAfter: number;
+}
diff --git a/packages/core/test-helpers/core-test-helpers-model-versions/tsconfig.json b/packages/core/test-helpers/core-test-helpers-model-versions/tsconfig.json
new file mode 100644
index 0000000000000..fe08f1cce0cea
--- /dev/null
+++ b/packages/core/test-helpers/core-test-helpers-model-versions/tsconfig.json
@@ -0,0 +1,37 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "jest",
+ "node"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": [
+ "@kbn/core-test-helpers-kbn-server",
+ "@kbn/config",
+ "@kbn/config-mocks",
+ "@kbn/repo-info",
+ "@kbn/core-saved-objects-migration-server-internal",
+ "@kbn/core-saved-objects-base-server-internal",
+ "@kbn/core-saved-objects-api-server-internal",
+ "@kbn/core-elasticsearch-server-internal",
+ "@kbn/core-elasticsearch-client-server-internal",
+ "@kbn/core-logging-server-internal",
+ "@kbn/core-saved-objects-server",
+ "@kbn/test",
+ "@kbn/logging",
+ "@kbn/core-elasticsearch-server",
+ "@kbn/core-root-server-internal",
+ "@kbn/core-saved-objects-api-server",
+ "@kbn/doc-links",
+ "@kbn/core-doc-links-server",
+ "@kbn/core-node-server",
+ ]
+}
diff --git a/packages/kbn-alerts-as-data-utils/src/field_maps/legacy_experimental_field_map.ts b/packages/kbn-alerts-as-data-utils/src/field_maps/legacy_experimental_field_map.ts
index b31727115b53c..520b682097460 100644
--- a/packages/kbn-alerts-as-data-utils/src/field_maps/legacy_experimental_field_map.ts
+++ b/packages/kbn-alerts-as-data-utils/src/field_maps/legacy_experimental_field_map.ts
@@ -11,6 +11,9 @@ import {
ALERT_EVALUATION_THRESHOLD,
ALERT_EVALUATION_VALUE,
ALERT_EVALUATION_VALUES,
+ ALERT_GROUP,
+ ALERT_GROUP_FIELD,
+ ALERT_GROUP_VALUE,
} from '@kbn/rule-data-utils';
export const legacyExperimentalFieldMap = {
@@ -27,6 +30,21 @@ export const legacyExperimentalFieldMap = {
required: false,
array: true,
},
+ [ALERT_GROUP]: {
+ type: 'object',
+ array: true,
+ required: false,
+ },
+ [ALERT_GROUP_FIELD]: {
+ type: 'keyword',
+ array: false,
+ required: false,
+ },
+ [ALERT_GROUP_VALUE]: {
+ type: 'keyword',
+ array: false,
+ required: false,
+ },
} as const;
export type ExperimentalRuleFieldMap = typeof legacyExperimentalFieldMap;
diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_apm_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_apm_schema.ts
index b6cdedfab73df..b15fe189b7e25 100644
--- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_apm_schema.ts
+++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_apm_schema.ts
@@ -76,6 +76,12 @@ const ObservabilityApmAlertOptional = rt.partial({
'kibana.alert.evaluation.threshold': schemaStringOrNumber,
'kibana.alert.evaluation.value': schemaStringOrNumber,
'kibana.alert.evaluation.values': schemaStringOrNumberArray,
+ 'kibana.alert.group': rt.array(
+ rt.partial({
+ field: schemaString,
+ value: schemaString,
+ })
+ ),
'processor.event': schemaString,
'service.environment': schemaString,
'service.language.name': schemaString,
diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_logs_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_logs_schema.ts
index 60d5f4102ce81..62b23dcab24ee 100644
--- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_logs_schema.ts
+++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_logs_schema.ts
@@ -74,6 +74,12 @@ const ObservabilityLogsAlertOptional = rt.partial({
'kibana.alert.evaluation.threshold': schemaStringOrNumber,
'kibana.alert.evaluation.value': schemaStringOrNumber,
'kibana.alert.evaluation.values': schemaStringOrNumberArray,
+ 'kibana.alert.group': rt.array(
+ rt.partial({
+ field: schemaString,
+ value: schemaString,
+ })
+ ),
});
// prettier-ignore
diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_metrics_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_metrics_schema.ts
index 7b731bf066c91..3806f2d096bd2 100644
--- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_metrics_schema.ts
+++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_metrics_schema.ts
@@ -74,6 +74,12 @@ const ObservabilityMetricsAlertOptional = rt.partial({
'kibana.alert.evaluation.threshold': schemaStringOrNumber,
'kibana.alert.evaluation.value': schemaStringOrNumber,
'kibana.alert.evaluation.values': schemaStringOrNumberArray,
+ 'kibana.alert.group': rt.array(
+ rt.partial({
+ field: schemaString,
+ value: schemaString,
+ })
+ ),
});
// prettier-ignore
diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_slo_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_slo_schema.ts
index 85378d662d43a..62ad6c82c6391 100644
--- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_slo_schema.ts
+++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_slo_schema.ts
@@ -73,6 +73,12 @@ const ObservabilitySloAlertOptional = rt.partial({
'kibana.alert.evaluation.threshold': schemaStringOrNumber,
'kibana.alert.evaluation.value': schemaStringOrNumber,
'kibana.alert.evaluation.values': schemaStringOrNumberArray,
+ 'kibana.alert.group': rt.array(
+ rt.partial({
+ field: schemaString,
+ value: schemaString,
+ })
+ ),
'slo.id': schemaString,
'slo.instanceId': schemaString,
'slo.revision': schemaStringOrNumber,
diff --git a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_uptime_schema.ts b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_uptime_schema.ts
index e9735c96a34e7..12a54768d71e4 100644
--- a/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_uptime_schema.ts
+++ b/packages/kbn-alerts-as-data-utils/src/schemas/generated/observability_uptime_schema.ts
@@ -77,6 +77,12 @@ const ObservabilityUptimeAlertOptional = rt.partial({
'kibana.alert.evaluation.threshold': schemaStringOrNumber,
'kibana.alert.evaluation.value': schemaStringOrNumber,
'kibana.alert.evaluation.values': schemaStringOrNumberArray,
+ 'kibana.alert.group': rt.array(
+ rt.partial({
+ field: schemaString,
+ value: schemaString,
+ })
+ ),
'monitor.id': schemaString,
'monitor.name': schemaString,
'monitor.type': schemaString,
diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json
index c18777c8b052d..2f8ad9cf0a18b 100644
--- a/packages/kbn-check-mappings-update-cli/current_mappings.json
+++ b/packages/kbn-check-mappings-update-cli/current_mappings.json
@@ -1370,6 +1370,41 @@
},
"category": {
"type": "keyword"
+ },
+ "customFields": {
+ "type": "nested",
+ "properties": {
+ "key": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "value": {
+ "type": "keyword",
+ "fields": {
+ "number": {
+ "type": "long",
+ "ignore_malformed": true
+ },
+ "boolean": {
+ "type": "boolean",
+ "ignore_malformed": true
+ },
+ "string": {
+ "type": "text"
+ },
+ "date": {
+ "type": "date",
+ "ignore_malformed": true
+ },
+ "ip": {
+ "type": "ip",
+ "ignore_malformed": true
+ }
+ }
+ }
+ }
}
}
},
diff --git a/packages/kbn-management/cards_navigation/src/consts.tsx b/packages/kbn-management/cards_navigation/src/consts.tsx
index 45deab542b0a8..03abf7e3cd407 100644
--- a/packages/kbn-management/cards_navigation/src/consts.tsx
+++ b/packages/kbn-management/cards_navigation/src/consts.tsx
@@ -82,7 +82,7 @@ export const appDefinitions: Record = {
category: appCategories.DATA,
description: i18n.translate('management.landing.withCardNavigation.mlDescription', {
defaultMessage:
- 'View, export, and import machine learning analytics and anomaly detection items.',
+ 'Identify, analyze, and process your data using advanced analysis techniques.',
}),
icon: ,
},
diff --git a/packages/kbn-openapi-generator/src/template_service/templates/zod_query_item.handlebars b/packages/kbn-openapi-generator/src/template_service/templates/zod_query_item.handlebars
index 0b718d941cbed..7fa146cd783e4 100644
--- a/packages/kbn-openapi-generator/src/template_service/templates/zod_query_item.handlebars
+++ b/packages/kbn-openapi-generator/src/template_service/templates/zod_query_item.handlebars
@@ -43,9 +43,17 @@
{{~/if~}}
{{~#if (eq type "integer")}}
-{{> zod_schema_item}}
+ z.coerce.number().int()
+ {{~#if minimum includeZero=true}}.min({{minimum}}){{/if~}}
+ {{~#if maximum includeZero=true}}.max({{maximum}}){{/if~}}
+ {{~#if (eq requiredBool false)}}.optional(){{/if~}}
+ {{~#if (defined default)}}.default({{{toJSON default}}}){{/if~}}
{{~/if~}}
{{~#if (eq type "number")}}
-{{> zod_schema_item}}
+ z.coerce.number()
+ {{~#if minimum includeZero=true}}.min({{minimum}}){{/if~}}
+ {{~#if maximum includeZero=true}}.max({{maximum}}){{/if~}}
+ {{~#if (eq requiredBool false)}}.optional(){{/if~}}
+ {{~#if (defined default)}}.default({{{toJSON default}}}){{/if~}}
{{~/if~}}
diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts
index 5f0570fa9542e..b387ab67750d5 100644
--- a/packages/kbn-rule-data-utils/src/technical_field_names.ts
+++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts
@@ -88,6 +88,9 @@ const ALERT_EVALUATION_THRESHOLD = `${ALERT_NAMESPACE}.evaluation.threshold` as
const ALERT_EVALUATION_VALUE = `${ALERT_NAMESPACE}.evaluation.value` as const;
const ALERT_CONTEXT = `${ALERT_NAMESPACE}.context` as const;
const ALERT_EVALUATION_VALUES = `${ALERT_NAMESPACE}.evaluation.values` as const;
+const ALERT_GROUP = `${ALERT_NAMESPACE}.group` as const;
+const ALERT_GROUP_FIELD = `${ALERT_GROUP}.field` as const;
+const ALERT_GROUP_VALUE = `${ALERT_GROUP}.value` as const;
// Fields pertaining to the rule associated with the alert
const ALERT_RULE_EXCEPTIONS_LIST = `${ALERT_RULE_NAMESPACE}.exceptions_list` as const;
@@ -129,6 +132,9 @@ const fields = {
ALERT_EVALUATION_THRESHOLD,
ALERT_EVALUATION_VALUE,
ALERT_EVALUATION_VALUES,
+ ALERT_GROUP,
+ ALERT_GROUP_FIELD,
+ ALERT_GROUP_VALUE,
ALERT_FLAPPING,
ALERT_MAINTENANCE_WINDOW_IDS,
ALERT_INSTANCE_ID,
@@ -200,6 +206,9 @@ export {
ALERT_EVALUATION_VALUE,
ALERT_CONTEXT,
ALERT_EVALUATION_VALUES,
+ ALERT_GROUP,
+ ALERT_GROUP_FIELD,
+ ALERT_GROUP_VALUE,
ALERT_RULE_EXCEPTIONS_LIST,
ALERT_RULE_NAMESPACE_FIELD,
ALERT_THREAT_FRAMEWORK,
diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
index ec5fd0ca2add5..423008ee1eece 100644
--- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
+++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
@@ -69,7 +69,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"canvas-element": "cdedc2123eb8a1506b87a56b0bcce60f4ec08bc8",
"canvas-workpad": "9d82aafb19586b119e5c9382f938abe28c26ca5c",
"canvas-workpad-template": "c077b0087346776bb3542b51e1385d172cb24179",
- "cases": "b43a8ce985c406167e1d115381805a48cb3b0e61",
+ "cases": "2392189ed338857d4815a4cef6051f9ad124d39d",
"cases-comments": "5cb0a421588831c2a950e50f486048b8aabbae25",
"cases-configure": "44ed7b8e0f44df39516b8870589b89e32224d2bf",
"cases-connector-mappings": "f9d1ac57e484e69506c36a8051e4d61f4a8cfd25",
diff --git a/src/core/server/integration_tests/saved_objects/migrations/zdt_2/sor_higher_version_docs.test.ts b/src/core/server/integration_tests/saved_objects/migrations/zdt_2/sor_higher_version_docs.test.ts
index e431c3607d983..fd423c36a54f6 100644
--- a/src/core/server/integration_tests/saved_objects/migrations/zdt_2/sor_higher_version_docs.test.ts
+++ b/src/core/server/integration_tests/saved_objects/migrations/zdt_2/sor_higher_version_docs.test.ts
@@ -8,57 +8,49 @@
import { pick, range } from 'lodash';
import Path from 'path';
-import fs from 'fs/promises';
-import { type TestElasticsearchUtils } from '@kbn/core-test-helpers-kbn-server';
import '../jest_matchers';
import { ISavedObjectsRepository } from '@kbn/core-saved-objects-api-server';
-import { SavedObjectsModelVersionMap } from '@kbn/core-saved-objects-server';
-import { getKibanaMigratorTestKit, startElasticsearch } from '../kibana_migrator_test_kit';
-import { delay, createType } from '../test_utils';
-import { getBaseMigratorParams } from '../fixtures/zdt_base.fixtures';
+import { createType } from '../test_utils';
import { SavedObjectsBulkCreateObject } from '@kbn/core-saved-objects-api-server';
+import { createModelVersionTestBed } from '@kbn/core-test-helpers-model-versions';
export const logFilePath = Path.join(__dirname, 'sor_higher.test.log');
+const modelVersionTestBed = createModelVersionTestBed();
+
describe('Higher version doc conversion', () => {
- let esServer: TestElasticsearchUtils['es'];
let repositoryV1: ISavedObjectsRepository;
let repositoryV2: ISavedObjectsRepository;
- const getTestType = ({ includeVersion2 }: { includeVersion2: boolean }) => {
- const modelVersions: SavedObjectsModelVersionMap = {
- 1: {
- changes: [],
- schemas: {
- forwardCompatibility: (attrs: any) => {
- return pick(attrs, 'text', 'bool');
+ const getTestType = () => {
+ return createType({
+ name: 'test-type',
+ switchToModelVersionAt: '8.0.0',
+ modelVersions: {
+ 1: {
+ changes: [],
+ schemas: {
+ forwardCompatibility: (attrs: any) => {
+ return pick(attrs, 'text', 'bool');
+ },
},
},
- },
- };
-
- if (includeVersion2) {
- modelVersions[2] = {
- changes: [
- {
- type: 'data_backfill',
- backfillFn: (document) => {
- return { attributes: { newField: 'someValue' } };
+ 2: {
+ changes: [
+ {
+ type: 'data_backfill',
+ backfillFn: (document) => {
+ return { attributes: { newField: 'someValue' } };
+ },
+ },
+ ],
+ schemas: {
+ forwardCompatibility: (attrs: any) => {
+ return pick(attrs, 'text', 'bool', 'newField');
},
- },
- ],
- schemas: {
- forwardCompatibility: (attrs: any) => {
- return pick(attrs, 'text', 'bool', 'newField');
},
},
- };
- }
-
- return createType({
- name: 'test-type',
- switchToModelVersionAt: '8.0.0',
- modelVersions,
+ },
mappings: {
dynamic: false,
properties: {
@@ -69,58 +61,34 @@ describe('Higher version doc conversion', () => {
});
};
- const createBaseline = async () => {
- const testTypeV1 = getTestType({ includeVersion2: false });
- const testTypeV2 = getTestType({ includeVersion2: true });
+ beforeAll(async () => {
+ await modelVersionTestBed.startES();
- const {
- runMigrations,
- savedObjectsRepository: savedObjectsRepositoryV1,
- client,
- } = await getKibanaMigratorTestKit({
- ...getBaseMigratorParams(),
+ const testkit = await modelVersionTestBed.prepareTestKit({
logFilePath,
- types: [testTypeV1],
+ savedObjectDefinitions: [
+ {
+ definition: getTestType(),
+ modelVersionBefore: 1,
+ modelVersionAfter: 2,
+ },
+ ],
+ objectsToCreateBetween: range(5).map((number) => ({
+ id: `doc-${number}`,
+ type: 'test-type',
+ attributes: {
+ text: `a_${number}`,
+ bool: true,
+ },
+ })),
});
- await runMigrations();
-
- const sampleAObjs = range(5).map((number) => ({
- id: `doc-${number}`,
- type: 'test-type',
- attributes: {
- text: `a_${number}`,
- bool: true,
- },
- }));
-
- await savedObjectsRepositoryV1.bulkCreate(sampleAObjs, { refresh: 'wait_for' });
-
- const { runMigrations: runMigrationsAgain, savedObjectsRepository: savedObjectsRepositoryV2 } =
- await getKibanaMigratorTestKit({
- ...getBaseMigratorParams(),
- logFilePath,
- types: [testTypeV2],
- });
- await runMigrationsAgain();
-
- // returns the repository for v1
- return { savedObjectsRepositoryV1, savedObjectsRepositoryV2, client };
- };
-
- beforeAll(async () => {
- await fs.unlink(logFilePath).catch(() => {});
- esServer = await startElasticsearch();
-
- const { savedObjectsRepositoryV1: sorV1, savedObjectsRepositoryV2: sorV2 } =
- await createBaseline();
- repositoryV1 = sorV1;
- repositoryV2 = sorV2;
+ repositoryV1 = testkit.repositoryBefore;
+ repositoryV2 = testkit.repositoryAfter;
});
afterAll(async () => {
- await esServer?.stop();
- await delay(10);
+ await modelVersionTestBed.stopES();
});
describe('#get', () => {
diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json
index 8232e102ddab2..2e63a6c845681 100644
--- a/src/core/tsconfig.json
+++ b/src/core/tsconfig.json
@@ -152,6 +152,7 @@
"@kbn/tooling-log",
"@kbn/stdio-dev-helpers",
"@kbn/safer-lodash-set",
+ "@kbn/core-test-helpers-model-versions",
],
"exclude": [
"target/**/*",
diff --git a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx
index f9f3efdd299f3..00b68c2abc547 100644
--- a/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx
@@ -31,15 +31,15 @@ import {
} from './url/search_sessions_integration';
import { DashboardAPI, DashboardRenderer } from '..';
import { type DashboardEmbedSettings } from './types';
-import { DASHBOARD_APP_ID } from '../dashboard_constants';
import { pluginServices } from '../services/plugin_services';
-import { DashboardTopNav } from './top_nav/dashboard_top_nav';
import { AwaitingDashboardAPI } from '../dashboard_container';
import { DashboardRedirect } from '../dashboard_container/types';
import { useDashboardMountContext } from './hooks/dashboard_mount_context';
+import { createDashboardEditUrl, DASHBOARD_APP_ID } from '../dashboard_constants';
import { useDashboardOutcomeValidation } from './hooks/use_dashboard_outcome_validation';
import { loadDashboardHistoryLocationState } from './locator/load_dashboard_history_location_state';
import type { DashboardCreationOptions } from '../dashboard_container/embeddable/dashboard_container_factory';
+import { DashboardTopNav } from '../dashboard_top_nav';
export interface DashboardAppProps {
history: History;
@@ -160,6 +160,10 @@ export function DashboardApp({
getInitialInput,
validateLoadedSavedObject: validateOutcome,
isEmbeddedExternally: Boolean(embedSettings), // embed settings are only sent if the dashboard URL has `embed=true`
+ getEmbeddableAppContext: (dashboardId) => ({
+ currentAppId: DASHBOARD_APP_ID,
+ getCurrentPath: () => `#${createDashboardEditUrl(dashboardId)}`,
+ }),
});
}, [
history,
@@ -192,9 +196,11 @@ export function DashboardApp({
{!showNoDataPage && (
<>
{dashboardAPI && (
-
-
-
+
)}
{getLegacyConflictWarning?.()}
diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx
index 63dd1d96d1169..0190bbaefa00b 100644
--- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx
@@ -5,7 +5,6 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
import { css } from '@emotion/react';
import React, { useCallback } from 'react';
import { METRIC_TYPE } from '@kbn/analytics';
@@ -21,7 +20,7 @@ import { EditorMenu } from './editor_menu';
import { useDashboardAPI } from '../dashboard_app';
import { pluginServices } from '../../services/plugin_services';
import { ControlsToolbarButton } from './controls_toolbar_button';
-import { DASHBOARD_APP_ID, DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants';
+import { DASHBOARD_UI_METRIC_ID } from '../../dashboard_constants';
import { dashboardReplacePanelActionStrings } from '../../dashboard_actions/_dashboard_actions_strings';
export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }) {
@@ -70,12 +69,13 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean }
stateTransferService.navigateToEditor(appId, {
path,
state: {
- originatingApp: DASHBOARD_APP_ID,
+ originatingApp: dashboard.getAppContext()?.currentAppId,
+ originatingPath: dashboard.getAppContext()?.getCurrentPath?.(),
searchSessionId: search.session.getSessionId(),
},
});
},
- [stateTransferService, search.session, trackUiMetric]
+ [stateTransferService, dashboard, search.session, trackUiMetric]
);
const createNewEmbeddable = useCallback(
diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx
index 643765bdfbab6..9c5aedb76c147 100644
--- a/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx
@@ -26,10 +26,12 @@ export const useDashboardMenuItems = ({
redirectTo,
isLabsShown,
setIsLabsShown,
+ showResetChange,
}: {
redirectTo: DashboardRedirect;
isLabsShown: boolean;
setIsLabsShown: Dispatch>;
+ showResetChange?: boolean;
}) => {
const [isSaveInProgress, setIsSaveInProgress] = useState(false);
@@ -276,32 +278,56 @@ export const useDashboardMenuItems = ({
const shareMenuItem = share ? [menuItems.share] : [];
const cloneMenuItem = showWriteControls ? [menuItems.clone] : [];
const editMenuItem = showWriteControls && !managed ? [menuItems.edit] : [];
+ const mayberesetChangesMenuItem = showResetChange ? [resetChangesMenuItem] : [];
+
return [
...labsMenuItem,
menuItems.fullScreen,
...shareMenuItem,
...cloneMenuItem,
- resetChangesMenuItem,
+ ...mayberesetChangesMenuItem,
...editMenuItem,
];
- }, [isLabsEnabled, menuItems, share, showWriteControls, managed, resetChangesMenuItem]);
+ }, [
+ isLabsEnabled,
+ menuItems,
+ share,
+ showWriteControls,
+ managed,
+ showResetChange,
+ resetChangesMenuItem,
+ ]);
const editModeTopNavConfig = useMemo(() => {
const labsMenuItem = isLabsEnabled ? [menuItems.labs] : [];
const shareMenuItem = share ? [menuItems.share] : [];
const editModeItems: TopNavMenuData[] = [];
+
if (lastSavedId) {
- editModeItems.push(
- menuItems.saveAs,
- menuItems.switchToViewMode,
- resetChangesMenuItem,
- menuItems.quickSave
- );
+ editModeItems.push(menuItems.saveAs, menuItems.switchToViewMode);
+
+ if (showResetChange) {
+ editModeItems.push(resetChangesMenuItem);
+ }
+
+ editModeItems.push(menuItems.quickSave);
} else {
editModeItems.push(menuItems.switchToViewMode, menuItems.saveAs);
}
return [...labsMenuItem, menuItems.settings, ...shareMenuItem, ...editModeItems];
- }, [lastSavedId, menuItems, share, resetChangesMenuItem, isLabsEnabled]);
+ }, [
+ isLabsEnabled,
+ menuItems.labs,
+ menuItems.share,
+ menuItems.settings,
+ menuItems.saveAs,
+ menuItems.switchToViewMode,
+ menuItems.quickSave,
+ share,
+ lastSavedId,
+ showResetChange,
+ resetChangesMenuItem,
+ ]);
return { viewModeTopNavConfig, editModeTopNavConfig };
};
diff --git a/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.tsx b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.tsx
index 050de4189c279..8767b5abe3567 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.tsx
@@ -24,7 +24,7 @@ import { ViewMode } from '@kbn/embeddable-plugin/public';
import { pluginServices } from '../../../services/plugin_services';
import { emptyScreenStrings } from '../../_dashboard_container_strings';
import { useDashboardContainer } from '../../embeddable/dashboard_container';
-import { DASHBOARD_UI_METRIC_ID, DASHBOARD_APP_ID } from '../../../dashboard_constants';
+import { DASHBOARD_UI_METRIC_ID } from '../../../dashboard_constants';
export function DashboardEmptyScreen() {
const {
@@ -44,6 +44,14 @@ export function DashboardEmptyScreen() {
[getVisTypeAliases]
);
+ const dashboardContainer = useDashboardContainer();
+ const isDarkTheme = useObservable(theme$)?.darkMode;
+ const isEditMode =
+ dashboardContainer.select((state) => state.explicitInput.viewMode) === ViewMode.EDIT;
+ const embeddableAppContext = dashboardContainer.getAppContext();
+ const originatingPath = embeddableAppContext?.getCurrentPath?.() ?? '';
+ const originatingApp = embeddableAppContext?.currentAppId;
+
const goToLens = useCallback(() => {
if (!lensAlias || !lensAlias.aliasPath) return;
const trackUiMetric = usageCollection.reportUiCounter?.bind(
@@ -57,16 +65,19 @@ export function DashboardEmptyScreen() {
getStateTransfer().navigateToEditor(lensAlias.aliasApp, {
path: lensAlias.aliasPath,
state: {
- originatingApp: DASHBOARD_APP_ID,
+ originatingApp,
+ originatingPath,
searchSessionId: search.session.getSessionId(),
},
});
- }, [getStateTransfer, lensAlias, search.session, usageCollection]);
-
- const dashboardContainer = useDashboardContainer();
- const isDarkTheme = useObservable(theme$)?.darkMode;
- const isEditMode =
- dashboardContainer.select((state) => state.explicitInput.viewMode) === ViewMode.EDIT;
+ }, [
+ getStateTransfer,
+ lensAlias,
+ originatingApp,
+ originatingPath,
+ search.session,
+ usageCollection,
+ ]);
// TODO replace these SVGs with versions from EuiIllustration as soon as it becomes available.
const imageUrl = basePath.prepend(
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
index 4c88d246f6ca3..0899fa0ebc97e 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
@@ -53,7 +53,7 @@ import { DASHBOARD_CONTAINER_TYPE } from '../..';
import { placePanel } from '../component/panel_placement';
import { pluginServices } from '../../services/plugin_services';
import { initializeDashboard } from './create/create_dashboard';
-import { DASHBOARD_LOADED_EVENT } from '../../dashboard_constants';
+import { DASHBOARD_APP_ID, DASHBOARD_LOADED_EVENT } from '../../dashboard_constants';
import { DashboardCreationOptions } from './dashboard_container_factory';
import { DashboardAnalyticsService } from '../../services/analytics/types';
import { DashboardViewport } from '../component/viewport/dashboard_viewport';
@@ -107,7 +107,6 @@ export class DashboardContainer extends Container void;
private cleanupStateTools: () => void;
@@ -185,6 +184,16 @@ export class DashboardContainer extends Container 'valid' | 'invalid' | 'redirected';
isEmbeddedExternally?: boolean;
+
+ getEmbeddableAppContext?: (dashboardId?: string) => EmbeddableAppContext;
}
export class DashboardContainerFactoryDefinition
diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/_dashboard_top_nav.scss b/src/plugins/dashboard/public/dashboard_top_nav/_dashboard_top_nav.scss
similarity index 100%
rename from src/plugins/dashboard/public/dashboard_app/top_nav/_dashboard_top_nav.scss
rename to src/plugins/dashboard/public/dashboard_top_nav/_dashboard_top_nav.scss
diff --git a/src/plugins/dashboard/public/dashboard_top_nav/dashboard_top_nav_with_context.tsx b/src/plugins/dashboard/public/dashboard_top_nav/dashboard_top_nav_with_context.tsx
new file mode 100644
index 0000000000000..ed2a7426697c1
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_top_nav/dashboard_top_nav_with_context.tsx
@@ -0,0 +1,27 @@
+/*
+ * 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 { DashboardAPIContext } from '../dashboard_app/dashboard_app';
+import { DashboardContainer } from '../dashboard_container';
+import {
+ InternalDashboardTopNav,
+ InternalDashboardTopNavProps,
+} from './internal_dashboard_top_nav';
+export interface DashboardTopNavProps extends InternalDashboardTopNavProps {
+ dashboardContainer: DashboardContainer;
+}
+
+export const DashboardTopNavWithContext = (props: DashboardTopNavProps) => (
+
+
+
+);
+
+// eslint-disable-next-line import/no-default-export
+export default DashboardTopNavWithContext;
diff --git a/src/plugins/dashboard/public/dashboard_top_nav/index.tsx b/src/plugins/dashboard/public/dashboard_top_nav/index.tsx
new file mode 100644
index 0000000000000..d0cfc496fcc3f
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_top_nav/index.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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, { Suspense } from 'react';
+import { servicesReady } from '../plugin';
+import { DashboardTopNavProps } from './dashboard_top_nav_with_context';
+
+const LazyDashboardTopNav = React.lazy(() =>
+ (async () => {
+ const modulePromise = import('./dashboard_top_nav_with_context');
+ const [module] = await Promise.all([modulePromise, servicesReady]);
+
+ return {
+ default: module.DashboardTopNavWithContext,
+ };
+ })().then((module) => module)
+);
+
+export const DashboardTopNav = (props: DashboardTopNavProps) => {
+ return (
+ }>
+
+
+ );
+};
diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx
similarity index 78%
rename from src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx
rename to src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx
index 67c51a19052d9..c2e0e273a572e 100644
--- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx
+++ b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx
@@ -18,36 +18,49 @@ import { ViewMode } from '@kbn/embeddable-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { TopNavMenuProps } from '@kbn/navigation-plugin/public';
import { EuiHorizontalRule, EuiIcon, EuiToolTipProps } from '@elastic/eui';
-
+import { EuiBreadcrumbProps } from '@elastic/eui/src/components/breadcrumbs/breadcrumb';
+import { MountPoint } from '@kbn/core/public';
import {
getDashboardTitle,
leaveConfirmStrings,
getDashboardBreadcrumb,
unsavedChangesBadgeStrings,
dashboardManagedBadge,
-} from '../_dashboard_app_strings';
-import { UI_SETTINGS } from '../../../common';
-import { useDashboardAPI } from '../dashboard_app';
-import { DashboardEmbedSettings } from '../types';
-import { pluginServices } from '../../services/plugin_services';
-import { useDashboardMenuItems } from './use_dashboard_menu_items';
-import { DashboardRedirect } from '../../dashboard_container/types';
-import { DashboardEditingToolbar } from './dashboard_editing_toolbar';
-import { useDashboardMountContext } from '../hooks/dashboard_mount_context';
-import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../../dashboard_constants';
-
+} from '../dashboard_app/_dashboard_app_strings';
+import { UI_SETTINGS } from '../../common';
+import { useDashboardAPI } from '../dashboard_app/dashboard_app';
+import { pluginServices } from '../services/plugin_services';
+import { useDashboardMenuItems } from '../dashboard_app/top_nav/use_dashboard_menu_items';
+import { DashboardEmbedSettings } from '../dashboard_app/types';
+import { DashboardEditingToolbar } from '../dashboard_app/top_nav/dashboard_editing_toolbar';
+import { useDashboardMountContext } from '../dashboard_app/hooks/dashboard_mount_context';
+import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../dashboard_constants';
import './_dashboard_top_nav.scss';
-export interface DashboardTopNavProps {
+import { DashboardRedirect } from '../dashboard_container/types';
+
+export interface InternalDashboardTopNavProps {
+ customLeadingBreadCrumbs?: EuiBreadcrumbProps[];
embedSettings?: DashboardEmbedSettings;
+ forceHideUnifiedSearch?: boolean;
redirectTo: DashboardRedirect;
+ setCustomHeaderActionMenu?: (menuMount: MountPoint | undefined) => void;
+ showBorderBottom?: boolean;
+ showResetChange?: boolean;
}
const LabsFlyout = withSuspense(LazyLabsFlyout, null);
-export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavProps) {
+export function InternalDashboardTopNav({
+ customLeadingBreadCrumbs = [],
+ embedSettings,
+ forceHideUnifiedSearch,
+ redirectTo,
+ setCustomHeaderActionMenu,
+ showBorderBottom = true,
+ showResetChange = true,
+}: InternalDashboardTopNavProps) {
const [isChromeVisible, setIsChromeVisible] = useState(false);
const [isLabsShown, setIsLabsShown] = useState(false);
-
const dashboardTitleRef = useRef(null);
/**
@@ -168,19 +181,33 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
// set only the dashboardTitleBreadcrumbs because the main breadcrumbs automatically come as part of the navigation config
serverless.setBreadcrumbs(dashboardTitleBreadcrumbs);
} else {
- // non-serverless regular breadcrumbs
- setBreadcrumbs([
- {
- text: getDashboardBreadcrumb(),
- 'data-test-subj': 'dashboardListingBreadcrumb',
- onClick: () => {
- redirectTo({ destination: 'listing' });
+ /**
+ * non-serverless regular breadcrumbs
+ * Dashboard embedded in other plugins (e.g. SecuritySolution)
+ * will have custom leading breadcrumbs for back to their app.
+ **/
+ setBreadcrumbs(
+ customLeadingBreadCrumbs.concat([
+ {
+ text: getDashboardBreadcrumb(),
+ 'data-test-subj': 'dashboardListingBreadcrumb',
+ onClick: () => {
+ redirectTo({ destination: 'listing' });
+ },
},
- },
- ...dashboardTitleBreadcrumbs,
- ]);
+ ...dashboardTitleBreadcrumbs,
+ ])
+ );
}
- }, [setBreadcrumbs, redirectTo, dashboardTitle, dashboard, viewMode, serverless]);
+ }, [
+ setBreadcrumbs,
+ redirectTo,
+ dashboardTitle,
+ dashboard,
+ viewMode,
+ serverless,
+ customLeadingBreadCrumbs,
+ ]);
/**
* Build app leave handler whenever hasUnsavedChanges changes
@@ -205,12 +232,6 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
};
}, [onAppLeave, getStateTransfer, hasUnsavedChanges, viewMode]);
- const { viewModeTopNavConfig, editModeTopNavConfig } = useDashboardMenuItems({
- redirectTo,
- isLabsShown,
- setIsLabsShown,
- });
-
const visibilityProps = useMemo(() => {
const shouldShowNavBarComponent = (forceShow: boolean): boolean =>
(forceShow || isChromeVisible) && !fullScreenMode;
@@ -218,14 +239,17 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
!forceHide && (filterManager.getFilters().length > 0 || !fullScreenMode);
const showTopNavMenu = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowTopNavMenu));
- const showQueryInput = shouldShowNavBarComponent(
- Boolean(embedSettings?.forceShowQueryInput || viewMode === ViewMode.PRINT)
- );
- const showDatePicker = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowDatePicker));
+ const showQueryInput = Boolean(forceHideUnifiedSearch)
+ ? false
+ : shouldShowNavBarComponent(
+ Boolean(embedSettings?.forceShowQueryInput || viewMode === ViewMode.PRINT)
+ );
+ const showDatePicker = Boolean(forceHideUnifiedSearch)
+ ? false
+ : shouldShowNavBarComponent(Boolean(embedSettings?.forceShowDatePicker));
const showFilterBar = shouldShowFilterBar(Boolean(embedSettings?.forceHideFilterBar));
const showQueryBar = showQueryInput || showDatePicker || showFilterBar;
const showSearchBar = showQueryBar || showFilterBar;
-
return {
showTopNavMenu,
showSearchBar,
@@ -233,7 +257,21 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
showQueryInput,
showDatePicker,
};
- }, [embedSettings, filterManager, fullScreenMode, isChromeVisible, viewMode]);
+ }, [
+ embedSettings,
+ filterManager,
+ forceHideUnifiedSearch,
+ fullScreenMode,
+ isChromeVisible,
+ viewMode,
+ ]);
+
+ const { viewModeTopNavConfig, editModeTopNavConfig } = useDashboardMenuItems({
+ redirectTo,
+ isLabsShown,
+ setIsLabsShown,
+ showResetChange,
+ });
UseUnmount(() => {
dashboard.clearOverlays();
@@ -301,7 +339,11 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
saveQueryMenuVisibility={allowSaveQuery ? 'allowed_by_app_privilege' : 'globally_managed'}
appName={LEGACY_DASHBOARD_APP_ID}
visible={viewMode !== ViewMode.PRINT}
- setMenuMountPoint={embedSettings || fullScreenMode ? undefined : setHeaderActionMenu}
+ setMenuMountPoint={
+ embedSettings || fullScreenMode
+ ? setCustomHeaderActionMenu ?? undefined
+ : setHeaderActionMenu
+ }
className={fullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined}
config={
visibilityProps.showTopNavMenu
@@ -327,7 +369,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
{viewMode === ViewMode.EDIT ? (
) : null}
-
+ {showBorderBottom && }
);
}
diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts
index 6882090df441a..89cc7b1aabed8 100644
--- a/src/plugins/dashboard/public/index.ts
+++ b/src/plugins/dashboard/public/index.ts
@@ -25,7 +25,7 @@ export {
export type { DashboardSetup, DashboardStart, DashboardFeatureFlagConfig } from './plugin';
export { DashboardListingTable } from './dashboard_listing';
-
+export { DashboardTopNav } from './dashboard_top_nav';
export {
type DashboardAppLocator,
type DashboardAppLocatorParams,
diff --git a/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx
index 3d823c215498b..93279e311b065 100644
--- a/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx
+++ b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx
@@ -49,14 +49,7 @@ const getEventStatus = (output: EmbeddableOutput): EmbeddablePhase => {
};
export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
- const {
- hideHeader,
- showShadow,
- embeddable,
- hideInspector,
- containerContext,
- onPanelStatusChange,
- } = panelProps;
+ const { hideHeader, showShadow, embeddable, hideInspector, onPanelStatusChange } = panelProps;
const [node, setNode] = useState();
const embeddableRoot: React.RefObject = useMemo(() => React.createRef(), []);
@@ -74,8 +67,7 @@ export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
const editPanel = new EditPanelAction(
embeddableStart.getEmbeddableFactory,
core.application,
- stateTransfer,
- containerContext?.getCurrentPath
+ stateTransfer
);
const actions: PanelUniversalActions = {
@@ -91,7 +83,7 @@ export const EmbeddablePanel = (panelProps: UnwrappedEmbeddablePanelProps) => {
};
if (!hideInspector) actions.inspectPanel = new InspectPanelAction(inspector);
return actions;
- }, [containerContext?.getCurrentPath, hideInspector]);
+ }, [hideInspector]);
/**
* Track panel status changes
diff --git a/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.test.tsx b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.test.tsx
index a1b1bf4df5ec4..33b1cc15a55bc 100644
--- a/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.test.tsx
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.test.tsx
@@ -46,12 +46,7 @@ test('is compatible when edit url is available, in edit mode and editable', asyn
test('redirects to app using state transfer', async () => {
applicationMock.currentAppId$ = of('superCoolCurrentApp');
const testPath = '/test-path';
- const action = new EditPanelAction(
- getFactory,
- applicationMock,
- stateTransferMock,
- () => testPath
- );
+ const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock);
const embeddable = new EditableEmbeddable(
{
id: '123',
@@ -62,6 +57,9 @@ test('redirects to app using state transfer', async () => {
true
);
embeddable.getOutput = jest.fn(() => ({ editApp: 'ultraVisualize', editPath: '/123' }));
+ embeddable.getAppContext = jest.fn().mockReturnValue({
+ getCurrentPath: () => testPath,
+ });
await action.execute({ embeddable });
expect(stateTransferMock.navigateToEditor).toHaveBeenCalledWith('ultraVisualize', {
path: '/123',
diff --git a/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts
index fe55b9a39158b..32e9fbac493aa 100644
--- a/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts
+++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts
@@ -45,8 +45,7 @@ export class EditPanelAction implements Action {
constructor(
private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'],
private readonly application: ApplicationStart,
- private readonly stateTransfer?: EmbeddableStateTransfer,
- private readonly getOriginatingPath?: () => string
+ private readonly stateTransfer?: EmbeddableStateTransfer
) {
if (this.application?.currentAppId$) {
this.application.currentAppId$
@@ -139,7 +138,7 @@ export class EditPanelAction implements Action {
if (app && path) {
if (this.currentAppId) {
- const originatingPath = this.getOriginatingPath?.();
+ const originatingPath = embeddable.getAppContext()?.getCurrentPath?.();
const state: EmbeddableEditorState = {
originatingApp: this.currentAppId,
diff --git a/src/plugins/embeddable/public/embeddable_panel/types.ts b/src/plugins/embeddable/public/embeddable_panel/types.ts
index 9a1b17d4a3a4d..03e29810d4056 100644
--- a/src/plugins/embeddable/public/embeddable_panel/types.ts
+++ b/src/plugins/embeddable/public/embeddable_panel/types.ts
@@ -19,11 +19,12 @@ import {
import { EmbeddableError } from '../lib/embeddables/i_embeddable';
import { EmbeddableContext, EmbeddableInput, EmbeddableOutput, IEmbeddable } from '..';
-export interface EmbeddableContainerContext {
+export interface EmbeddableAppContext {
/**
* Current app's path including query and hash starting from {appId}
*/
getCurrentPath?: () => string;
+ currentAppId?: string;
}
/**
@@ -53,7 +54,6 @@ export interface EmbeddablePanelProps {
hideHeader?: boolean;
hideInspector?: boolean;
showNotifications?: boolean;
- containerContext?: EmbeddableContainerContext;
actionPredicate?: (actionId: string) => boolean;
onPanelStatusChange?: (info: EmbeddablePhaseEvent) => void;
getActions?: UiActionsService['getTriggerCompatibleActions'];
diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts
index 0e3650ea8a8a4..ebe3b1a11af03 100644
--- a/src/plugins/embeddable/public/index.ts
+++ b/src/plugins/embeddable/public/index.ts
@@ -100,7 +100,7 @@ export {
export type {
EmbeddablePhase,
EmbeddablePhaseEvent,
- EmbeddableContainerContext,
+ EmbeddableAppContext,
} from './embeddable_panel/types';
export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './lib/attribute_service';
diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
index d145bfb3c1ae0..ac1c8462b5bf3 100644
--- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
+++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
@@ -17,6 +17,7 @@ import { IContainer } from '../containers';
import { EmbeddableError, EmbeddableOutput, IEmbeddable } from './i_embeddable';
import { EmbeddableInput, ViewMode } from '../../../common/types';
import { genericEmbeddableInputIsEqual, omitGenericEmbeddableInput } from './diff_embeddable_input';
+import { EmbeddableAppContext } from '../../embeddable_panel/types';
function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) {
if (input.hidePanelTitles) return '';
@@ -102,6 +103,10 @@ export abstract class Embeddable<
.subscribe((title) => this.renderComplete.setTitle(title));
}
+ public getAppContext(): EmbeddableAppContext | undefined {
+ return this.parent?.getAppContext();
+ }
+
public reportsEmbeddableLoad() {
return false;
}
diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
index f371208271623..92d0309688e76 100644
--- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
@@ -11,6 +11,7 @@ import { ErrorLike } from '@kbn/expressions-plugin/common';
import { Adapters } from '../types';
import { IContainer } from '../containers/i_container';
import { EmbeddableInput } from '../../../common/types';
+import { EmbeddableAppContext } from '../../embeddable_panel/types';
export type EmbeddableError = ErrorLike;
export type { EmbeddableInput };
@@ -181,6 +182,11 @@ export interface IEmbeddable<
*/
getRoot(): IEmbeddable | IContainer;
+ /**
+ * Returns the context of this embeddable's container, or undefined.
+ */
+ getAppContext(): EmbeddableAppContext | undefined;
+
/**
* Renders the embeddable at the given node.
* @param domNode
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 9f21fe96ae942..98ee10281509a 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -542,6 +542,8 @@
"@kbn/core-test-helpers-http-setup-browser/*": ["packages/core/test-helpers/core-test-helpers-http-setup-browser/*"],
"@kbn/core-test-helpers-kbn-server": ["packages/core/test-helpers/core-test-helpers-kbn-server"],
"@kbn/core-test-helpers-kbn-server/*": ["packages/core/test-helpers/core-test-helpers-kbn-server/*"],
+ "@kbn/core-test-helpers-model-versions": ["packages/core/test-helpers/core-test-helpers-model-versions"],
+ "@kbn/core-test-helpers-model-versions/*": ["packages/core/test-helpers/core-test-helpers-model-versions/*"],
"@kbn/core-test-helpers-so-type-serializer": ["packages/core/test-helpers/core-test-helpers-so-type-serializer"],
"@kbn/core-test-helpers-so-type-serializer/*": ["packages/core/test-helpers/core-test-helpers-so-type-serializer/*"],
"@kbn/core-test-helpers-test-utils": ["packages/core/test-helpers/core-test-helpers-test-utils"],
diff --git a/x-pack/packages/ml/agg_utils/src/build_sampler_aggregation.ts b/x-pack/packages/ml/agg_utils/src/build_sampler_aggregation.ts
index b430590245f64..3183d4b82fb86 100644
--- a/x-pack/packages/ml/agg_utils/src/build_sampler_aggregation.ts
+++ b/x-pack/packages/ml/agg_utils/src/build_sampler_aggregation.ts
@@ -11,6 +11,11 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
* Wraps the supplied aggregations in a sampler aggregation.
* A supplied samplerShardSize (the shard_size parameter of the sampler aggregation)
* of less than 1 indicates no sampling, and the aggs are returned as-is.
+ *
+ * @param aggs - The aggregations to be wrapped in the sampler aggregation.
+ * @param shardSize - The shard size parameter for the sampler aggregation.
+ * A value less than 1 indicates no sampling.
+ * @returns The wrapped aggregations.
*/
export function buildSamplerAggregation(
aggs: any,
diff --git a/x-pack/packages/ml/agg_utils/src/fetch_agg_intervals.ts b/x-pack/packages/ml/agg_utils/src/fetch_agg_intervals.ts
index 671e68d58ec31..d8a4c81f95e93 100644
--- a/x-pack/packages/ml/agg_utils/src/fetch_agg_intervals.ts
+++ b/x-pack/packages/ml/agg_utils/src/fetch_agg_intervals.ts
@@ -23,6 +23,17 @@ const MAX_CHART_COLUMNS = 20;
/**
* Returns aggregation intervals for the supplied document fields.
+ *
+ * @param client - The Elasticsearch client.
+ * @param indexPattern - The index pattern to search.
+ * @param query - The query to filter documents.
+ * @param fields - An array of field definitions for which aggregation intervals are requested.
+ * @param samplerShardSize - The shard size parameter for sampling aggregations. A value less than 1 indicates no sampling.
+ * @param runtimeMappings - Optional runtime mappings to apply.
+ * @param abortSignal - Optional AbortSignal for canceling the request.
+ * @param randomSamplerProbability - Optional probability value for random sampling.
+ * @param randomSamplerSeed - Optional seed value for random sampling.
+ * @returns A promise that resolves to a map of aggregation intervals for the specified fields.
*/
export const fetchAggIntervals = async (
client: ElasticsearchClient,
diff --git a/x-pack/packages/ml/agg_utils/src/fetch_histograms_for_fields.ts b/x-pack/packages/ml/agg_utils/src/fetch_histograms_for_fields.ts
index 1baa98317008a..39908328ef34c 100644
--- a/x-pack/packages/ml/agg_utils/src/fetch_histograms_for_fields.ts
+++ b/x-pack/packages/ml/agg_utils/src/fetch_histograms_for_fields.ts
@@ -41,20 +41,55 @@ interface AggTerms {
};
}
+/**
+ * Represents an item in numeric data.
+ * @interface
+ */
interface NumericDataItem {
+ /**
+ * The numeric key.
+ */
key: number;
+
+ /**
+ * An optional string representation of the key.
+ */
key_as_string?: string;
+
+ /**
+ * The document count associated with the key.
+ */
doc_count: number;
}
/**
- * Interface to describe the data structure returned for numeric based charts.
+ * Interface describing the data structure returned for numeric-based charts.
+ * @interface
*/
export interface NumericChartData {
+ /**
+ * An array of data points, each represented by a NumericDataItem.
+ */
data: NumericDataItem[];
+
+ /**
+ * The identifier for the data set.
+ */
id: string;
+
+ /**
+ * The interval value for the data.
+ */
interval: number;
+
+ /**
+ * An array of statistics values, typically [min, max].
+ */
stats: [number, number];
+
+ /**
+ * The type of chart, which is 'numeric'.
+ */
type: 'numeric';
}
@@ -62,6 +97,9 @@ export interface NumericChartData {
* Numeric based histogram field interface, limited to `date` and `number`.
*/
export interface NumericHistogramField extends HistogramField {
+ /**
+ * The type of the numeric histogram field.
+ */
type: KBN_FIELD_TYPES.DATE | KBN_FIELD_TYPES.NUMBER;
}
type NumericHistogramFieldWithColumnStats = NumericHistogramField & NumericColumnStats;
@@ -139,6 +177,7 @@ export type FieldsForHistograms = Array<
* @param fields the fields the histograms should be generated for
* @param samplerShardSize shard_size parameter of the sampler aggregation
* @param runtimeMappings optional runtime mappings
+ * @param abortSignal optional abort signal
* @param randomSamplerProbability optional random sampler probability
* @param randomSamplerSeed optional random sampler seed
* @returns an array of histogram data for each supplied field
diff --git a/x-pack/packages/ml/agg_utils/src/get_sampler_aggregations_response_path.ts b/x-pack/packages/ml/agg_utils/src/get_sampler_aggregations_response_path.ts
index 48a9e5051cacd..b3b864ecebfda 100644
--- a/x-pack/packages/ml/agg_utils/src/get_sampler_aggregations_response_path.ts
+++ b/x-pack/packages/ml/agg_utils/src/get_sampler_aggregations_response_path.ts
@@ -5,10 +5,14 @@
* 2.0.
*/
-// Returns the path of aggregations in the elasticsearch response, as an array,
-// depending on whether sampling is being used.
-// A supplied samplerShardSize (the shard_size parameter of the sampler aggregation)
-// of less than 1 indicates no sampling, and an empty array is returned.
+/**
+ * Returns the path of aggregations in the Elasticsearch response as an array,
+ * depending on whether sampling is being used.
+ *
+ * @param samplerShardSize - The shard size parameter of the sampler aggregation.
+ * A value less than 1 indicates no sampling.
+ * @returns An array representing the path of aggregations in the response.
+ */
export function getSamplerAggregationsResponsePath(samplerShardSize: number): string[] {
return samplerShardSize > 0 ? ['sample'] : [];
}
diff --git a/x-pack/packages/ml/agg_utils/src/types.ts b/x-pack/packages/ml/agg_utils/src/types.ts
index 9c55d5480381b..92b1d03ea9d5e 100644
--- a/x-pack/packages/ml/agg_utils/src/types.ts
+++ b/x-pack/packages/ml/agg_utils/src/types.ts
@@ -7,19 +7,33 @@
import { KBN_FIELD_TYPES } from '@kbn/field-types';
+/**
+ * Represents a field-based cardinality aggregation configuration.
+ * @interface
+ */
interface FieldAggCardinality {
+ /** The field on which the cardinality aggregation is applied. */
field: string;
+
+ /** Optional property percent. */
percent?: any;
}
+/**
+ * Represents a script-based cardinality aggregation configuration.
+ * @interface
+ */
interface ScriptAggCardinality {
+ /** The script used for the cardinality aggregation. */
script: any;
}
/**
* Interface for cardinality aggregation.
+ * @interface
*/
export interface AggCardinality {
+ /** The cardinality aggregation configuration */
cardinality: FieldAggCardinality | ScriptAggCardinality;
}
@@ -27,19 +41,28 @@ export interface AggCardinality {
* Field/value pair definition.
*/
export interface FieldValuePair {
+ /** The field name. */
fieldName: string;
- // For dynamic fieldValues we only identify fields as `string`,
- // but for example `http.response.status_code` which is part of
- // of the list of predefined field candidates is of type long/number.
+ /**
+ * For dynamic fieldValues we only identify fields as `string`,
+ * but for example `http.response.status_code` which is part of
+ * of the list of predefined field candidates is of type long/number
+ */
fieldValue: string | number;
}
/**
- * Interface to describe attributes used for histograms.
+ * Interface describing attributes used for numeric histograms.
+ * @interface
*/
export interface NumericColumnStats {
+ /** The interval value for the histogram. */
interval: number;
+
+ /** The minimum value in the histogram. */
min: number;
+
+ /** The maximum value in the histogram. */
max: number;
}
@@ -49,69 +72,130 @@ export interface NumericColumnStats {
export type NumericColumnStatsMap = Record;
/**
- * Parameters to identify which histogram data needs to be generated for a field.
+ * Represents parameters used to identify which histogram data needs to be generated for a field.
+ * @interface
*/
export interface HistogramField {
+ /**
+ * The name of the field for which histogram data is generated.
+ */
fieldName: string;
+
+ /**
+ * The type of the field, using Kibana field types.
+ */
type: KBN_FIELD_TYPES;
}
/**
- * Significant term meta data for a field/value pair.
- * Note this is used as a custom type within Log Rate Analysis
- * for a p-value based variant, not a generic significant terms
- * aggregation type.
+ * Represents significant term metadata for a field/value pair.
+ * This interface is used as a custom type within Log Rate Analysis
+ * for a p-value based variant, not related to the generic
+ * significant terms aggregation type.
+ *
+ * @interface
+ * @extends FieldValuePair
*/
export interface SignificantTerm extends FieldValuePair {
+ /** The document count for the significant term. */
doc_count: number;
+
+ /** The background count for the significant term. */
bg_count: number;
+
+ /** The total document count for all terms. */
total_doc_count: number;
+
+ /** The total background count for all terms. */
total_bg_count: number;
+
+ /** The score associated with the significant term. */
score: number;
+
+ /** The p-value for the significant term, or null if not available. */
pValue: number | null;
+
+ /** The normalized score for the significant term. */
normalizedScore: number;
+
+ /** An optional histogram of significant term items. */
histogram?: SignificantTermHistogramItem[];
+
+ /** Indicates if the significant term is unique within a group. */
unique?: boolean;
}
/**
- * Significant term histogram data item.
+ * Represents a data item in a significant term histogram.
+ * @interface
*/
export interface SignificantTermHistogramItem {
+ /** The document count for this item in the overall context. */
doc_count_overall: number;
+
+ /** The document count for this item in the significant term context. */
doc_count_significant_term: number;
+
+ /** The numeric key associated with this item. */
key: number;
+
+ /** The string representation of the key. */
key_as_string: string;
}
/**
- * Histogram data for a field/value pair.
+ * Represents histogram data for a field/value pair.
+ * @interface
*/
export interface SignificantTermHistogram extends FieldValuePair {
+ /** An array of significant term histogram items. */
histogram: SignificantTermHistogramItem[];
}
/**
- * Histogram data for a group of field/value pairs.
+ * Represents histogram data for a group of field/value pairs.
+ * @interface
*/
export interface SignificantTermGroupHistogram {
+ /** The identifier for the group. */
id: string;
+
+ /** An array of significant term histogram items. */
histogram: SignificantTermHistogramItem[];
}
+/**
+ * Represents an item in a significant term group.
+ * @interface
+ */
export interface SignificantTermGroupItem extends FieldValuePair {
+ /** The document count associated with this item. */
docCount: number;
+
+ /** The p-value for this item, or null if not available. */
pValue: number | null;
+
+ /** An optional number of duplicates. */
duplicate?: number;
}
/**
- * Tree leaves
+ * Represents a significant term group.
+ * @interface
*/
export interface SignificantTermGroup {
+ /** The identifier for the item. */
id: string;
+
+ /** An array of significant term group items. */
group: SignificantTermGroupItem[];
+
+ /** The document count associated with this item. */
docCount: number;
+
+ /** The p-value for this item, or null if not available. */
pValue: number | null;
+
+ /** An optional array of significant term histogram items. */
histogram?: SignificantTermHistogramItem[];
}
diff --git a/x-pack/packages/ml/agg_utils/src/validate_number.ts b/x-pack/packages/ml/agg_utils/src/validate_number.ts
index 15da7b250c324..0d89cfbfab434 100644
--- a/x-pack/packages/ml/agg_utils/src/validate_number.ts
+++ b/x-pack/packages/ml/agg_utils/src/validate_number.ts
@@ -8,20 +8,50 @@
import { memoize } from 'lodash';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
+/**
+ * Represents the result of number validation.
+ * @interface
+ */
export interface NumberValidationResult {
+ /** The minimum allowed value. */
min: boolean;
+
+ /** The maximum allowed value. */
max: boolean;
+
+ /** Boolean flag to allow integer values only. */
integerOnly: boolean;
}
/**
- * Validate if a number is greater than a specified minimum & lesser than specified maximum
+ * An interface describing conditions for validating numbers.
+ * @interface
*/
-export function numberValidator(conditions?: {
+interface NumberValidatorConditions {
+ /**
+ * The minimum value for validation.
+ */
min?: number;
+
+ /**
+ * The maximum value for validation.
+ */
max?: number;
+
+ /**
+ * Indicates whether only integer values are valid.
+ */
integerOnly?: boolean;
-}) {
+}
+
+/**
+ * Validate if a number is within specified minimum and maximum bounds.
+ *
+ * @param conditions - An optional object containing validation conditions.
+ * @returns validation results.
+ * @throws {Error} If the provided conditions are invalid (min is greater than max).
+ */
+export function numberValidator(conditions?: NumberValidatorConditions) {
if (
conditions?.min !== undefined &&
conditions.max !== undefined &&
diff --git a/x-pack/packages/security-solution/upselling/service/types.ts b/x-pack/packages/security-solution/upselling/service/types.ts
index 2f5a06c391471..d14f39ac9796a 100644
--- a/x-pack/packages/security-solution/upselling/service/types.ts
+++ b/x-pack/packages/security-solution/upselling/service/types.ts
@@ -14,6 +14,7 @@ export type SectionUpsellings = Partial {
});
});
+ describe('global_labels', () => {
+ const task = tasks.find((t) => t.name === 'global_labels');
+
+ it('returns count of global labels when present', async () => {
+ const fieldCaps = jest.fn().mockResolvedValue({
+ indices: [
+ '.ds-metrics-apm.service_destination.1m-default-2023.09.26-000005',
+ '.ds-metrics-apm.service_summary.1m-default-2023.09.26-000005',
+ '.ds-metrics-apm.service_transaction.1m-default-2023.09.26-000005',
+ '.ds-metrics-apm.transaction.1m-default-2023.09.26-000005',
+ ],
+ fields: {
+ 'labels.telemetry_auto_version': {
+ keyword: {
+ type: 'keyword',
+ metadata_field: false,
+ searchable: true,
+ aggregatable: true,
+ },
+ },
+ labels: {
+ object: {
+ type: 'object',
+ metadata_field: false,
+ searchable: false,
+ aggregatable: false,
+ },
+ },
+ },
+ });
+
+ expect(
+ await task?.executor({ indices, telemetryClient: { fieldCaps } } as any)
+ ).toEqual({
+ counts: {
+ global_labels: {
+ '1d': 1,
+ },
+ },
+ });
+ });
+
+ it('returns 0 count of global labels when not present', async () => {
+ const fieldCaps = jest.fn().mockResolvedValue({
+ indices: [],
+ fields: {},
+ });
+
+ expect(
+ await task?.executor({ indices, telemetryClient: { fieldCaps } } as any)
+ ).toEqual({
+ counts: {
+ global_labels: {
+ '1d': 0,
+ },
+ },
+ });
+ });
+ });
+
describe('cloud', () => {
const task = tasks.find((t) => t.name === 'cloud');
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
index d5e9203446c38..b980c79e261cd 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
@@ -589,6 +589,43 @@ export const tasks: TelemetryTask[] = [
};
},
},
+ {
+ name: 'global_labels',
+ executor: async ({ telemetryClient }) => {
+ const metricConsistingGlobalLabels = [
+ 'service_summary',
+ 'service_transaction',
+ 'transaction',
+ 'service_destination',
+ ];
+
+ const index = metricConsistingGlobalLabels
+ .map((metric) => `metrics-apm.${metric}*`)
+ .join(',');
+
+ const response = await telemetryClient.fieldCaps({
+ index,
+ fields: 'labels.*',
+ expand_wildcards: 'all',
+ index_filter: range1d,
+ });
+
+ const globalLabelCount = Object.keys(response.fields).length;
+
+ // Skip the top level Labels field which is sometimes present in the response
+ const count = response.fields?.labels
+ ? globalLabelCount - 1
+ : globalLabelCount;
+
+ return {
+ counts: {
+ global_labels: {
+ '1d': count,
+ },
+ },
+ };
+ },
+ },
{
name: 'services',
executor: async ({ indices, telemetryClient }) => {
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts
index ccc6dd4ab01b7..7c5ca6c78c7b3 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts
@@ -758,6 +758,15 @@ export const apmSchema: MakeSchemaFrom = {
},
},
},
+ global_labels: {
+ '1d': {
+ type: 'long',
+ _meta: {
+ description:
+ 'Total number of global labels used for creating aggregation keys for internal metrics computed from indices which received data in the last 24 hours',
+ },
+ },
+ },
max_transaction_groups_per_service: {
'1d': {
type: 'long',
@@ -1145,6 +1154,17 @@ export const apmSchema: MakeSchemaFrom = {
},
},
},
+ global_labels: {
+ took: {
+ ms: {
+ type: 'long',
+ _meta: {
+ description:
+ 'Execution time in milliseconds for the "global_labels" task',
+ },
+ },
+ },
+ },
services: {
took: {
ms: {
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/telemetry_client.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/telemetry_client.ts
index 8afcd019233de..4941dd40633d9 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/telemetry_client.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/telemetry_client.ts
@@ -46,6 +46,10 @@ export interface TelemetryClient {
path: string;
method: 'get';
}) => Promise;
+
+ fieldCaps(
+ params: estypes.FieldCapsRequest
+ ): Promise;
}
export async function getTelemetryClient({
@@ -69,5 +73,9 @@ export async function getTelemetryClient({
unwrapEsResponse(
esClient.asInternalUser.transport.request(params, { meta: true })
),
+ fieldCaps: (params) =>
+ unwrapEsResponse(
+ esClient.asInternalUser.fieldCaps(params, { meta: true })
+ ),
};
}
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts
index eebce519e73d5..b4beb768ae334 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts
@@ -111,6 +111,7 @@ export interface APMUsage {
services: TimeframeMap1d;
environments: TimeframeMap1d;
span_destination_service_resource: TimeframeMap1d;
+ global_labels: TimeframeMap1d;
};
cardinality: {
client: { geo: { country_iso_code: { rum: TimeframeMap1d } } };
@@ -223,6 +224,7 @@ export interface APMUsage {
| 'host'
| 'processor_events'
| 'agent_configuration'
+ | 'global_labels'
| 'services'
| 'versions'
| 'groupings'
diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx
index 51c6cdc54131d..ec602510fd9f0 100644
--- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx
+++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx
@@ -17,13 +17,13 @@ import {
isErrorEmbeddable,
EmbeddablePanel,
} from '@kbn/embeddable-plugin/public';
-import type { EmbeddableContainerContext } from '@kbn/embeddable-plugin/public';
+import type { EmbeddableAppContext } from '@kbn/embeddable-plugin/public';
import { StartDeps } from '../../plugin';
import { EmbeddableExpression } from '../../expression_types/embeddable';
import { RendererStrings } from '../../../i18n';
import { embeddableInputToExpression } from './embeddable_input_to_expression';
import { RendererFactory, EmbeddableInput } from '../../../types';
-import { CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib';
+import { CANVAS_APP, CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib';
const { embeddable: strings } = RendererStrings;
@@ -41,18 +41,19 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => {
return null;
}
- const embeddableContainerContext: EmbeddableContainerContext = {
+ const canvasAppContext: EmbeddableAppContext = {
getCurrentPath: () => {
const urlToApp = core.application.getUrlForApp(currentAppId);
const inAppPath = window.location.pathname.replace(urlToApp, '');
return inAppPath + window.location.search + window.location.hash;
},
+ currentAppId: CANVAS_APP,
};
- return (
-
- );
+ embeddable.getAppContext = () => canvasAppContext;
+
+ return ;
};
return (embeddableObject: IEmbeddable) => {
diff --git a/x-pack/plugins/cases/common/constants/index.ts b/x-pack/plugins/cases/common/constants/index.ts
index 01c8543493f7a..603fa70e7d0d4 100644
--- a/x-pack/plugins/cases/common/constants/index.ts
+++ b/x-pack/plugins/cases/common/constants/index.ts
@@ -128,6 +128,11 @@ export const MAX_CASES_TO_UPDATE = 100 as const;
export const MAX_BULK_CREATE_ATTACHMENTS = 100 as const;
export const MAX_USER_ACTIONS_PER_CASE = 10000 as const;
export const MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES = 100 as const;
+export const MAX_CUSTOM_FIELDS_PER_CASE = 10 as const;
+export const MAX_CUSTOM_FIELD_KEY_LENGTH = 36 as const; // uuidv4 length
+export const MAX_CUSTOM_FIELD_LABEL_LENGTH = 50 as const;
+export const MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH = 160 as const;
+export const MAX_CUSTOM_FIELD_TEXT_VALUE_ITEMS = 10 as const;
/**
* Cases features
diff --git a/x-pack/plugins/cases/common/schema/index.ts b/x-pack/plugins/cases/common/schema/index.ts
index bc3e6af9ea584..b38d499c8c04c 100644
--- a/x-pack/plugins/cases/common/schema/index.ts
+++ b/x-pack/plugins/cases/common/schema/index.ts
@@ -153,3 +153,26 @@ export const limitedNumberSchema = ({ fieldName, min, max }: LimitedSchemaType)
}),
rt.identity
);
+
+export interface RegexStringSchemaType {
+ codec: rt.Type;
+ pattern: string;
+ message: string;
+}
+
+export const regexStringRt = ({ codec, pattern, message }: RegexStringSchemaType) =>
+ new rt.Type(
+ 'RegexString',
+ codec.is,
+ (input, context) =>
+ either.chain(codec.validate(input, context), (value) => {
+ const regex = new RegExp(pattern, 'g');
+
+ if (!regex.test(value)) {
+ return rt.failure(input, context, message);
+ }
+
+ return rt.success(value);
+ }),
+ rt.identity
+ );
diff --git a/x-pack/plugins/cases/common/types/api/case/v1.test.ts b/x-pack/plugins/cases/common/types/api/case/v1.test.ts
index 91676b7ca6510..ce27fe5070b2c 100644
--- a/x-pack/plugins/cases/common/types/api/case/v1.test.ts
+++ b/x-pack/plugins/cases/common/types/api/case/v1.test.ts
@@ -16,12 +16,16 @@ import {
MAX_LENGTH_PER_TAG,
MAX_TITLE_LENGTH,
MAX_CATEGORY_LENGTH,
+ MAX_CUSTOM_FIELDS_PER_CASE,
+ MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH,
} from '../../../constants';
import { PathReporter } from 'io-ts/lib/PathReporter';
import { AttachmentType } from '../../domain/attachment/v1';
+import type { Case } from '../../domain/case/v1';
import { CaseSeverity, CaseStatuses } from '../../domain/case/v1';
import { ConnectorTypes } from '../../domain/connector/v1';
import { CasesStatusRequestRt, CasesStatusResponseRt } from '../stats/v1';
+import type { CasePostRequest } from './v1';
import {
AllReportersFindRequestRt,
CasePatchRequestRt,
@@ -37,8 +41,9 @@ import {
CasesFindResponseRt,
CasesPatchRequestRt,
} from './v1';
+import { CustomFieldTypes } from '../../domain/custom_field/v1';
-const basicCase = {
+const basicCase: Case = {
owner: 'cases',
closed_at: null,
closed_by: null,
@@ -96,289 +101,398 @@ const basicCase = {
// damaged_raccoon uid
assignees: [{ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }],
category: null,
+ customFields: [
+ {
+ key: 'first_custom_field_key',
+ type: CustomFieldTypes.TEXT,
+ value: ['this is a text field value', 'this is second'],
+ },
+ {
+ key: 'second_custom_field_key',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ {
+ key: 'second_custom_field_key',
+ type: CustomFieldTypes.TEXT,
+ value: ['www.example.com'],
+ },
+ ],
};
-describe('Status', () => {
- describe('CasesStatusRequestRt', () => {
- const defaultRequest = {
- from: '2022-04-28T15:18:00.000Z',
- to: '2022-04-28T15:22:00.000Z',
- owner: 'cases',
- };
+describe('CasePostRequestRt', () => {
+ const defaultRequest: CasePostRequest = {
+ description: 'A description',
+ tags: ['new', 'case'],
+ title: 'My new case',
+ connector: {
+ id: '123',
+ name: 'My connector',
+ type: ConnectorTypes.jira,
+ fields: { issueType: 'Task', priority: 'High', parent: null },
+ },
+ settings: {
+ syncAlerts: true,
+ },
+ owner: 'cases',
+ severity: CaseSeverity.LOW,
+ assignees: [{ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }],
+ customFields: [
+ {
+ key: 'first_custom_field_key',
+ type: CustomFieldTypes.TEXT,
+ value: ['this is a text field value', 'this is second'],
+ },
+ {
+ key: 'second_custom_field_key',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ],
+ };
- it('has expected attributes in request', () => {
- const query = CasesStatusRequestRt.decode(defaultRequest);
+ it('has expected attributes in request', () => {
+ const query = CasePostRequestRt.decode(defaultRequest);
- expect(query).toStrictEqual({
- _tag: 'Right',
- right: defaultRequest,
- });
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: defaultRequest,
});
+ });
- it('has removes foo:bar attributes from request', () => {
- const query = CasesStatusRequestRt.decode({ ...defaultRequest, foo: 'bar' });
+ it('removes foo:bar attributes from request', () => {
+ const query = CasePostRequestRt.decode({ ...defaultRequest, foo: 'bar' });
- expect(query).toStrictEqual({
- _tag: 'Right',
- right: defaultRequest,
- });
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: defaultRequest,
});
});
- describe('CasesStatusResponseRt', () => {
- const defaultResponse = {
- count_closed_cases: 1,
- count_in_progress_cases: 2,
- count_open_cases: 1,
- };
-
- it('has expected attributes in response', () => {
- const query = CasesStatusResponseRt.decode(defaultResponse);
+ it('removes foo:bar attributes from connector', () => {
+ const query = CasePostRequestRt.decode({
+ ...defaultRequest,
+ connector: { ...defaultRequest.connector, foo: 'bar' },
+ });
- expect(query).toStrictEqual({
- _tag: 'Right',
- right: defaultResponse,
- });
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: defaultRequest,
});
+ });
- it('removes foo:bar attributes from response', () => {
- const query = CasesStatusResponseRt.decode({ ...defaultResponse, foo: 'bar' });
+ it(`throws an error when the assignees are more than ${MAX_ASSIGNEES_PER_CASE}`, async () => {
+ const assignees = Array(MAX_ASSIGNEES_PER_CASE + 1).fill({ uid: 'foobar' });
- expect(query).toStrictEqual({
- _tag: 'Right',
- right: defaultResponse,
- });
- });
+ expect(
+ PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, assignees }))
+ ).toContain('The length of the field assignees is too long. Array must be of length <= 10.');
});
- describe('CasePostRequestRt', () => {
- const defaultRequest = {
- description: 'A description',
- tags: ['new', 'case'],
- title: 'My new case',
- connector: {
- id: '123',
- name: 'My connector',
- type: ConnectorTypes.jira,
- fields: { issueType: 'Task', priority: 'High', parent: null },
- },
- settings: {
- syncAlerts: true,
- },
- owner: 'cases',
- severity: CaseSeverity.LOW,
- assignees: [{ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }],
+ it('does not throw an error with empty assignees', async () => {
+ expect(
+ PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, assignees: [] }))
+ ).toContain('No errors!');
+ });
+
+ it('does not throw an error with undefined assignees', async () => {
+ const { assignees, ...rest } = defaultRequest;
+
+ expect(PathReporter.report(CasePostRequestRt.decode(rest))).toContain('No errors!');
+ });
+
+ it(`throws an error when the description contains more than ${MAX_DESCRIPTION_LENGTH} characters`, async () => {
+ const description = 'a'.repeat(MAX_DESCRIPTION_LENGTH + 1);
+
+ expect(
+ PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, description }))
+ ).toContain('The length of the description is too long. The maximum length is 30000.');
+ });
+
+ it(`throws an error when there are more than ${MAX_TAGS_PER_CASE} tags`, async () => {
+ const tags = Array(MAX_TAGS_PER_CASE + 1).fill('foobar');
+
+ expect(PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, tags }))).toContain(
+ 'The length of the field tags is too long. Array must be of length <= 200.'
+ );
+ });
+
+ it(`throws an error when the a tag is more than ${MAX_LENGTH_PER_TAG} characters`, async () => {
+ const tag = 'a'.repeat(MAX_LENGTH_PER_TAG + 1);
+
+ expect(
+ PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, tags: [tag] }))
+ ).toContain('The length of the tag is too long. The maximum length is 256.');
+ });
+
+ it(`throws an error when the title contains more than ${MAX_TITLE_LENGTH} characters`, async () => {
+ const title = 'a'.repeat(MAX_TITLE_LENGTH + 1);
+
+ expect(PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, title }))).toContain(
+ 'The length of the title is too long. The maximum length is 160.'
+ );
+ });
+
+ it(`throws an error when the category contains more than ${MAX_CATEGORY_LENGTH} characters`, async () => {
+ const category = 'a'.repeat(MAX_CATEGORY_LENGTH + 1);
+
+ expect(
+ PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, category }))
+ ).toContain('The length of the category is too long. The maximum length is 50.');
+ });
+
+ it('removes foo:bar attributes from customFields', () => {
+ const customField = {
+ key: 'first_custom_field_key',
+ type: 'text',
+ value: ['this is a text field value', 'this is second'],
};
- it('has expected attributes in request', () => {
- const query = CasePostRequestRt.decode(defaultRequest);
+ const query = CasePostRequestRt.decode({
+ ...defaultRequest,
+ customFields: [{ ...customField, foo: 'bar' }],
+ });
- expect(query).toStrictEqual({
- _tag: 'Right',
- right: defaultRequest,
- });
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest, customFields: [{ ...customField }] },
});
+ });
- it('removes foo:bar attributes from request', () => {
- const query = CasePostRequestRt.decode({ ...defaultRequest, foo: 'bar' });
+ it('removes foo:bar attributes from field inside customFields', () => {
+ const customField = {
+ key: 'first_custom_field_key',
+ type: 'text',
+ value: ['this is a text field value', 'this is second'],
+ };
- expect(query).toStrictEqual({
- _tag: 'Right',
- right: defaultRequest,
- });
+ const query = CasePostRequestRt.decode({
+ ...defaultRequest,
+ customFields: [{ ...customField, foo: 'bar' }],
});
- it('removes foo:bar attributes from connector', () => {
- const query = CasePostRequestRt.decode({
- ...defaultRequest,
- connector: { ...defaultRequest.connector, foo: 'bar' },
- });
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest, customFields: [{ ...customField }] },
+ });
+ });
- expect(query).toStrictEqual({
- _tag: 'Right',
- right: defaultRequest,
- });
+ it(`limits customFields to ${MAX_CUSTOM_FIELDS_PER_CASE}`, () => {
+ const customFields = Array(MAX_CUSTOM_FIELDS_PER_CASE + 1).fill({
+ key: 'first_custom_field_key',
+ type: 'text',
+ value: ['this is a text field value', 'this is second'],
});
- it(`throws an error when the assignees are more than ${MAX_ASSIGNEES_PER_CASE}`, async () => {
- const assignees = Array(MAX_ASSIGNEES_PER_CASE + 1).fill({ uid: 'foobar' });
+ expect(
+ PathReporter.report(
+ CasePostRequestRt.decode({
+ ...defaultRequest,
+ customFields,
+ })
+ )
+ ).toContain(
+ `The length of the field customFields is too long. Array must be of length <= ${MAX_CUSTOM_FIELDS_PER_CASE}.`
+ );
+ });
+
+ it('does not throw an error with undefined customFields', async () => {
+ const { customFields, ...rest } = defaultRequest;
- expect(
- PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, assignees }))
- ).toContain('The length of the field assignees is too long. Array must be of length <= 10.');
+ expect(PathReporter.report(CasePostRequestRt.decode(rest))).toContain('No errors!');
+ });
+
+ it(`throws an error when a text customFields is longer than ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => {
+ expect(
+ PathReporter.report(
+ CasePostRequestRt.decode({
+ ...defaultRequest,
+ customFields: [
+ {
+ key: 'first_custom_field_key',
+ type: 'text',
+ value: ['#'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1)],
+ },
+ ],
+ })
+ )
+ ).toContain(
+ `The length of the value is too long. The maximum length is ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}.`
+ );
+ });
+});
+
+describe('CasesFindRequestRt', () => {
+ const defaultRequest = {
+ tags: ['new', 'case'],
+ status: CaseStatuses.open,
+ severity: CaseSeverity.LOW,
+ assignees: ['damaged_racoon'],
+ reporters: ['damaged_racoon'],
+ defaultSearchOperator: 'AND',
+ from: 'now',
+ page: '1',
+ perPage: '10',
+ search: 'search text',
+ searchFields: ['title', 'description'],
+ to: '1w',
+ sortOrder: 'desc',
+ sortField: 'createdAt',
+ owner: 'cases',
+ };
+
+ it('has expected attributes in request', () => {
+ const query = CasesFindRequestRt.decode(defaultRequest);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest, page: 1, perPage: 10 },
});
+ });
- it('does not throw an error with empty assignees', async () => {
- expect(
- PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, assignees: [] }))
- ).toContain('No errors!');
+ it('removes foo:bar attributes from request', () => {
+ const query = CasesFindRequestRt.decode({ ...defaultRequest, foo: 'bar' });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest, page: 1, perPage: 10 },
});
+ });
- it('does not throw an error with undefined assignees', async () => {
- const { assignees, ...rest } = defaultRequest;
+ const searchFields = Object.keys(CasesFindRequestSearchFieldsRt.keys);
- expect(PathReporter.report(CasePostRequestRt.decode(rest))).toContain('No errors!');
+ it.each(searchFields)('succeeds with %s as searchFields', (field) => {
+ const query = CasesFindRequestRt.decode({ ...defaultRequest, searchFields: field });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest, searchFields: field, page: 1, perPage: 10 },
});
+ });
- it(`throws an error when the description contains more than ${MAX_DESCRIPTION_LENGTH} characters`, async () => {
- const description = 'a'.repeat(MAX_DESCRIPTION_LENGTH + 1);
+ const sortFields = Object.keys(CasesFindRequestSortFieldsRt.keys);
+ it.each(sortFields)('succeeds with %s as sortField', (sortField) => {
+ const query = CasesFindRequestRt.decode({ ...defaultRequest, sortField });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest, sortField, page: 1, perPage: 10 },
+ });
+ });
+
+ it('removes rootSearchField when passed', () => {
+ expect(
+ PathReporter.report(
+ CasesFindRequestRt.decode({ ...defaultRequest, rootSearchField: ['foobar'] })
+ )
+ ).toContain('No errors!');
+ });
+
+ describe('errors', () => {
+ it('throws error when invalid searchField passed', () => {
expect(
- PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, description }))
- ).toContain('The length of the description is too long. The maximum length is 30000.');
+ PathReporter.report(
+ CasesFindRequestRt.decode({ ...defaultRequest, searchFields: 'foobar' })
+ )
+ ).not.toContain('No errors!');
});
- it(`throws an error when there are more than ${MAX_TAGS_PER_CASE} tags`, async () => {
- const tags = Array(MAX_TAGS_PER_CASE + 1).fill('foobar');
+ it('throws error when invalid sortField passed', () => {
+ expect(
+ PathReporter.report(CasesFindRequestRt.decode({ ...defaultRequest, sortField: 'foobar' }))
+ ).not.toContain('No errors!');
+ });
- expect(PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, tags }))).toContain(
- 'The length of the field tags is too long. Array must be of length <= 200.'
+ it('succeeds when valid parameters passed', () => {
+ expect(PathReporter.report(CasesFindRequestRt.decode(defaultRequest))).toContain(
+ 'No errors!'
);
});
- it(`throws an error when the a tag is more than ${MAX_LENGTH_PER_TAG} characters`, async () => {
- const tag = 'a'.repeat(MAX_LENGTH_PER_TAG + 1);
+ it(`throws an error when the category array has ${MAX_CATEGORY_FILTER_LENGTH} items`, async () => {
+ const category = Array(MAX_CATEGORY_FILTER_LENGTH + 1).fill('foobar');
- expect(
- PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, tags: [tag] }))
- ).toContain('The length of the tag is too long. The maximum length is 256.');
+ expect(PathReporter.report(CasesFindRequestRt.decode({ category }))).toContain(
+ 'The length of the field category is too long. Array must be of length <= 100.'
+ );
});
- it(`throws an error when the title contains more than ${MAX_TITLE_LENGTH} characters`, async () => {
- const title = 'a'.repeat(MAX_TITLE_LENGTH + 1);
+ it(`throws an error when the tags array has ${MAX_TAGS_FILTER_LENGTH} items`, async () => {
+ const tags = Array(MAX_TAGS_FILTER_LENGTH + 1).fill('foobar');
- expect(PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, title }))).toContain(
- 'The length of the title is too long. The maximum length is 160.'
+ expect(PathReporter.report(CasesFindRequestRt.decode({ tags }))).toContain(
+ 'The length of the field tags is too long. Array must be of length <= 100.'
);
});
- it(`throws an error when the category contains more than ${MAX_CATEGORY_LENGTH} characters`, async () => {
- const category = 'a'.repeat(MAX_CATEGORY_LENGTH + 1);
+ it(`throws an error when the assignees array has ${MAX_ASSIGNEES_FILTER_LENGTH} items`, async () => {
+ const assignees = Array(MAX_ASSIGNEES_FILTER_LENGTH + 1).fill('foobar');
- expect(
- PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, category }))
- ).toContain('The length of the category is too long. The maximum length is 50.');
+ expect(PathReporter.report(CasesFindRequestRt.decode({ assignees }))).toContain(
+ 'The length of the field assignees is too long. Array must be of length <= 100.'
+ );
+ });
+
+ it(`throws an error when the reporters array has ${MAX_REPORTERS_FILTER_LENGTH} items`, async () => {
+ const reporters = Array(MAX_REPORTERS_FILTER_LENGTH + 1).fill('foobar');
+
+ expect(PathReporter.report(CasesFindRequestRt.decode({ reporters }))).toContain(
+ 'The length of the field reporters is too long. Array must be of length <= 100.'
+ );
});
});
+});
- describe('CasesFindRequestRt', () => {
+describe('Status', () => {
+ describe('CasesStatusRequestRt', () => {
const defaultRequest = {
- tags: ['new', 'case'],
- status: CaseStatuses.open,
- severity: CaseSeverity.LOW,
- assignees: ['damaged_racoon'],
- reporters: ['damaged_racoon'],
- defaultSearchOperator: 'AND',
- from: 'now',
- page: '1',
- perPage: '10',
- search: 'search text',
- searchFields: ['title', 'description'],
- to: '1w',
- sortOrder: 'desc',
- sortField: 'createdAt',
+ from: '2022-04-28T15:18:00.000Z',
+ to: '2022-04-28T15:22:00.000Z',
owner: 'cases',
};
it('has expected attributes in request', () => {
- const query = CasesFindRequestRt.decode(defaultRequest);
+ const query = CasesStatusRequestRt.decode(defaultRequest);
expect(query).toStrictEqual({
_tag: 'Right',
- right: { ...defaultRequest, page: 1, perPage: 10 },
+ right: defaultRequest,
});
});
- it('removes foo:bar attributes from request', () => {
- const query = CasesFindRequestRt.decode({ ...defaultRequest, foo: 'bar' });
+ it('has removes foo:bar attributes from request', () => {
+ const query = CasesStatusRequestRt.decode({ ...defaultRequest, foo: 'bar' });
expect(query).toStrictEqual({
_tag: 'Right',
- right: { ...defaultRequest, page: 1, perPage: 10 },
+ right: defaultRequest,
});
});
+ });
- const searchFields = Object.keys(CasesFindRequestSearchFieldsRt.keys);
+ describe('CasesStatusResponseRt', () => {
+ const defaultResponse = {
+ count_closed_cases: 1,
+ count_in_progress_cases: 2,
+ count_open_cases: 1,
+ };
- it.each(searchFields)('succeeds with %s as searchFields', (field) => {
- const query = CasesFindRequestRt.decode({ ...defaultRequest, searchFields: field });
+ it('has expected attributes in response', () => {
+ const query = CasesStatusResponseRt.decode(defaultResponse);
expect(query).toStrictEqual({
_tag: 'Right',
- right: { ...defaultRequest, searchFields: field, page: 1, perPage: 10 },
+ right: defaultResponse,
});
});
- const sortFields = Object.keys(CasesFindRequestSortFieldsRt.keys);
-
- it.each(sortFields)('succeeds with %s as sortField', (sortField) => {
- const query = CasesFindRequestRt.decode({ ...defaultRequest, sortField });
+ it('removes foo:bar attributes from response', () => {
+ const query = CasesStatusResponseRt.decode({ ...defaultResponse, foo: 'bar' });
expect(query).toStrictEqual({
_tag: 'Right',
- right: { ...defaultRequest, sortField, page: 1, perPage: 10 },
- });
- });
-
- it('removes rootSearchField when passed', () => {
- expect(
- PathReporter.report(
- CasesFindRequestRt.decode({ ...defaultRequest, rootSearchField: ['foobar'] })
- )
- ).toContain('No errors!');
- });
-
- describe('errors', () => {
- it('throws error when invalid searchField passed', () => {
- expect(
- PathReporter.report(
- CasesFindRequestRt.decode({ ...defaultRequest, searchFields: 'foobar' })
- )
- ).not.toContain('No errors!');
- });
-
- it('throws error when invalid sortField passed', () => {
- expect(
- PathReporter.report(CasesFindRequestRt.decode({ ...defaultRequest, sortField: 'foobar' }))
- ).not.toContain('No errors!');
- });
-
- it('succeeds when valid parameters passed', () => {
- expect(PathReporter.report(CasesFindRequestRt.decode(defaultRequest))).toContain(
- 'No errors!'
- );
- });
-
- it(`throws an error when the category array has ${MAX_CATEGORY_FILTER_LENGTH} items`, async () => {
- const category = Array(MAX_CATEGORY_FILTER_LENGTH + 1).fill('foobar');
-
- expect(PathReporter.report(CasesFindRequestRt.decode({ category }))).toContain(
- 'The length of the field category is too long. Array must be of length <= 100.'
- );
- });
-
- it(`throws an error when the tags array has ${MAX_TAGS_FILTER_LENGTH} items`, async () => {
- const tags = Array(MAX_TAGS_FILTER_LENGTH + 1).fill('foobar');
-
- expect(PathReporter.report(CasesFindRequestRt.decode({ tags }))).toContain(
- 'The length of the field tags is too long. Array must be of length <= 100.'
- );
- });
-
- it(`throws an error when the assignees array has ${MAX_ASSIGNEES_FILTER_LENGTH} items`, async () => {
- const assignees = Array(MAX_ASSIGNEES_FILTER_LENGTH + 1).fill('foobar');
-
- expect(PathReporter.report(CasesFindRequestRt.decode({ assignees }))).toContain(
- 'The length of the field assignees is too long. Array must be of length <= 100.'
- );
- });
-
- it(`throws an error when the reporters array has ${MAX_REPORTERS_FILTER_LENGTH} items`, async () => {
- const reporters = Array(MAX_REPORTERS_FILTER_LENGTH + 1).fill('foobar');
-
- expect(PathReporter.report(CasesFindRequestRt.decode({ reporters }))).toContain(
- 'The length of the field reporters is too long. Array must be of length <= 100.'
- );
+ right: defaultResponse,
});
});
});
@@ -478,6 +592,18 @@ describe('CasePatchRequestRt', () => {
id: 'basic-case-id',
version: 'WzQ3LDFd',
description: 'Updated description',
+ customFields: [
+ {
+ key: 'first_custom_field_key',
+ type: 'text',
+ value: ['this is a text field value', 'this is second'],
+ },
+ {
+ key: 'second_custom_field_key',
+ type: 'toggle',
+ value: true,
+ },
+ ],
};
it('has expected attributes in request', () => {
@@ -555,6 +681,44 @@ describe('CasePatchRequestRt', () => {
PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, category }))
).toContain('The length of the category is too long. The maximum length is 50.');
});
+
+ it(`limits customFields to ${MAX_CUSTOM_FIELDS_PER_CASE}`, () => {
+ const customFields = Array(MAX_CUSTOM_FIELDS_PER_CASE + 1).fill({
+ key: 'first_custom_field_key',
+ type: 'text',
+ value: ['this is a text field value', 'this is second'],
+ });
+
+ expect(
+ PathReporter.report(
+ CasePatchRequestRt.decode({
+ ...defaultRequest,
+ customFields,
+ })
+ )
+ ).toContain(
+ `The length of the field customFields is too long. Array must be of length <= ${MAX_CUSTOM_FIELDS_PER_CASE}.`
+ );
+ });
+
+ it(`throws an error when a text customFields is longer than ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => {
+ expect(
+ PathReporter.report(
+ CasePatchRequestRt.decode({
+ ...defaultRequest,
+ customFields: [
+ {
+ key: 'first_custom_field_key',
+ type: 'text',
+ value: ['#'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1)],
+ },
+ ],
+ })
+ )
+ ).toContain(
+ `The length of the value is too long. The maximum length is ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}.`
+ );
+ });
});
describe('CasesPatchRequestRt', () => {
diff --git a/x-pack/plugins/cases/common/types/api/case/v1.ts b/x-pack/plugins/cases/common/types/api/case/v1.ts
index 7a92c1f32ca12..f808080cc2f0d 100644
--- a/x-pack/plugins/cases/common/types/api/case/v1.ts
+++ b/x-pack/plugins/cases/common/types/api/case/v1.ts
@@ -21,6 +21,9 @@ import {
MAX_BULK_GET_CASES,
MAX_CATEGORY_FILTER_LENGTH,
MAX_ASSIGNEES_PER_CASE,
+ MAX_CUSTOM_FIELDS_PER_CASE,
+ MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH,
+ MAX_CUSTOM_FIELD_TEXT_VALUE_ITEMS,
} from '../../../constants';
import {
limitedStringSchema,
@@ -28,6 +31,7 @@ import {
NonEmptyString,
paginationSchema,
} from '../../../schema';
+import { CaseCustomFieldToggleRt, CustomFieldTextTypeRt } from '../../domain';
import {
CaseRt,
CaseSettingsRt,
@@ -40,10 +44,35 @@ import { CaseConnectorRt } from '../../domain/connector/v1';
import { CaseUserProfileRt, UserRt } from '../../domain/user/v1';
import { CasesStatusResponseRt } from '../stats/v1';
+const CaseCustomFieldWithValidationValueRt = limitedArraySchema({
+ codec: limitedStringSchema({
+ fieldName: 'value',
+ min: 0,
+ max: MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH,
+ }),
+ fieldName: 'value',
+ min: 0,
+ max: MAX_CUSTOM_FIELD_TEXT_VALUE_ITEMS,
+});
+
+const CaseCustomFieldTextWithValidationRt = rt.strict({
+ key: rt.string,
+ type: CustomFieldTextTypeRt,
+ value: rt.union([CaseCustomFieldWithValidationValueRt, rt.null]),
+});
+
+const CustomFieldRt = rt.union([CaseCustomFieldTextWithValidationRt, CaseCustomFieldToggleRt]);
+
+const CustomFieldsRt = limitedArraySchema({
+ codec: CustomFieldRt,
+ fieldName: 'customFields',
+ min: 0,
+ max: MAX_CUSTOM_FIELDS_PER_CASE,
+});
+
/**
* Create case
*/
-
export const CasePostRequestRt = rt.intersection([
rt.strict({
/**
@@ -104,6 +133,10 @@ export const CasePostRequestRt = rt.intersection([
limitedStringSchema({ fieldName: 'category', min: 1, max: MAX_CATEGORY_LENGTH }),
rt.null,
]),
+ /**
+ * The list of custom field values of the case.
+ */
+ customFields: CustomFieldsRt,
})
),
]);
@@ -358,6 +391,10 @@ export const CasePatchRequestRt = rt.intersection([
limitedStringSchema({ fieldName: 'category', min: 1, max: MAX_CATEGORY_LENGTH }),
rt.null,
]),
+ /**
+ * Custom fields of the case
+ */
+ customFields: CustomFieldsRt,
})
),
/**
@@ -448,3 +485,4 @@ export type GetReportersResponse = rt.TypeOf;
export type CasesBulkGetRequest = rt.TypeOf;
export type CasesBulkGetResponse = rt.TypeOf;
export type GetRelatedCasesByAlertResponse = rt.TypeOf;
+export type CaseRequestCustomFields = rt.TypeOf;
diff --git a/x-pack/plugins/cases/common/types/api/configure/v1.test.ts b/x-pack/plugins/cases/common/types/api/configure/v1.test.ts
index d5756a606d43b..b69e7701db69c 100644
--- a/x-pack/plugins/cases/common/types/api/configure/v1.test.ts
+++ b/x-pack/plugins/cases/common/types/api/configure/v1.test.ts
@@ -5,12 +5,23 @@
* 2.0.
*/
+import { PathReporter } from 'io-ts/lib/PathReporter';
+import { v4 as uuidv4 } from 'uuid';
+import {
+ MAX_CUSTOM_FIELDS_PER_CASE,
+ MAX_CUSTOM_FIELD_KEY_LENGTH,
+ MAX_CUSTOM_FIELD_LABEL_LENGTH,
+} from '../../../constants';
import { ConnectorTypes } from '../../domain/connector/v1';
+import { CustomFieldTypes } from '../../domain/custom_field/v1';
import {
CaseConfigureRequestParamsRt,
ConfigurationPatchRequestRt,
ConfigurationRequestRt,
GetConfigurationFindRequestRt,
+ CustomFieldConfigurationWithoutTypeRt,
+ TextCustomFieldConfigurationRt,
+ ToggleCustomFieldConfigurationRt,
} from './v1';
describe('configure', () => {
@@ -37,6 +48,47 @@ describe('configure', () => {
});
});
+ it('has expected attributes in request with customFields', () => {
+ const request = {
+ ...defaultRequest,
+ customFields: [
+ {
+ key: 'text_custom_field',
+ label: 'Text custom field',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ {
+ key: 'toggle_custom_field',
+ label: 'Toggle custom field',
+ type: CustomFieldTypes.TOGGLE,
+ required: false,
+ },
+ ],
+ };
+ const query = ConfigurationRequestRt.decode(request);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: request,
+ });
+ });
+
+ it(`limits customFields to ${MAX_CUSTOM_FIELDS_PER_CASE}`, () => {
+ const customFields = new Array(MAX_CUSTOM_FIELDS_PER_CASE + 1).fill({
+ key: 'text_custom_field',
+ label: 'Text custom field',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ });
+
+ expect(
+ PathReporter.report(ConfigurationRequestRt.decode({ ...defaultRequest, customFields }))[0]
+ ).toContain(
+ `The length of the field customFields is too long. Array must be of length <= ${MAX_CUSTOM_FIELDS_PER_CASE}`
+ );
+ });
+
it('removes foo:bar attributes from request', () => {
const query = ConfigurationRequestRt.decode({ ...defaultRequest, foo: 'bar' });
@@ -63,6 +115,49 @@ describe('configure', () => {
});
});
+ it('has expected attributes in request with customFields', () => {
+ const request = {
+ ...defaultRequest,
+ customFields: [
+ {
+ key: 'text_custom_field',
+ label: 'Text custom field',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ {
+ key: 'toggle_custom_field',
+ label: 'Toggle custom field',
+ type: CustomFieldTypes.TOGGLE,
+ required: false,
+ },
+ ],
+ };
+ const query = ConfigurationPatchRequestRt.decode(request);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: request,
+ });
+ });
+
+ it(`limits customFields to ${MAX_CUSTOM_FIELDS_PER_CASE}`, () => {
+ const customFields = new Array(MAX_CUSTOM_FIELDS_PER_CASE + 1).fill({
+ key: 'text_custom_field',
+ label: 'Text custom field',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ });
+
+ expect(
+ PathReporter.report(
+ ConfigurationPatchRequestRt.decode({ ...defaultRequest, customFields })
+ )[0]
+ ).toContain(
+ `The length of the field customFields is too long. Array must be of length <= ${MAX_CUSTOM_FIELDS_PER_CASE}`
+ );
+ });
+
it('removes foo:bar attributes from request', () => {
const query = ConfigurationPatchRequestRt.decode({ ...defaultRequest, foo: 'bar' });
@@ -120,4 +215,136 @@ describe('configure', () => {
});
});
});
+
+ describe('CustomFieldConfigurationWithoutTypeRt', () => {
+ const defaultRequest = {
+ key: 'custom_field_key',
+ label: 'Custom field label',
+ required: false,
+ };
+
+ it('has expected attributes in request', () => {
+ const query = CustomFieldConfigurationWithoutTypeRt.decode(defaultRequest);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+
+ it('removes foo:bar attributes from request', () => {
+ const query = CustomFieldConfigurationWithoutTypeRt.decode({ ...defaultRequest, foo: 'bar' });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+
+ it('limits key to 36 characters', () => {
+ const longKey = 'x'.repeat(MAX_CUSTOM_FIELD_KEY_LENGTH + 1);
+
+ expect(
+ PathReporter.report(
+ CustomFieldConfigurationWithoutTypeRt.decode({ ...defaultRequest, key: longKey })
+ )
+ ).toContain('The length of the key is too long. The maximum length is 36.');
+ });
+
+ it('returns an error if they key is not in the expected format', () => {
+ const key = 'Not a proper key';
+
+ expect(
+ PathReporter.report(
+ CustomFieldConfigurationWithoutTypeRt.decode({ ...defaultRequest, key })
+ )
+ ).toContain(`Key must be lower case, a-z, 0-9, '_', and '-' are allowed`);
+ });
+
+ it('accepts a uuid as a key', () => {
+ const key = uuidv4();
+
+ const query = CustomFieldConfigurationWithoutTypeRt.decode({ ...defaultRequest, key });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest, key },
+ });
+ });
+
+ it('accepts a slug as a key', () => {
+ const key = 'abc_key-1';
+
+ const query = CustomFieldConfigurationWithoutTypeRt.decode({ ...defaultRequest, key });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest, key },
+ });
+ });
+
+ it('limits label to 50 characters', () => {
+ const longLabel = 'x'.repeat(MAX_CUSTOM_FIELD_LABEL_LENGTH + 1);
+
+ expect(
+ PathReporter.report(
+ CustomFieldConfigurationWithoutTypeRt.decode({ ...defaultRequest, label: longLabel })
+ )
+ ).toContain('The length of the label is too long. The maximum length is 50.');
+ });
+ });
+
+ describe('TextCustomFieldConfigurationRt', () => {
+ const defaultRequest = {
+ key: 'my_text_custom_field',
+ label: 'Text Custom Field',
+ type: CustomFieldTypes.TEXT,
+ required: true,
+ };
+
+ it('has expected attributes in request', () => {
+ const query = TextCustomFieldConfigurationRt.decode(defaultRequest);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+
+ it('removes foo:bar attributes from request', () => {
+ const query = TextCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+ });
+
+ describe('ToggleCustomFieldConfigurationRt', () => {
+ const defaultRequest = {
+ key: 'my_toggle_custom_field',
+ label: 'Toggle Custom Field',
+ type: CustomFieldTypes.TOGGLE,
+ required: false,
+ };
+
+ it('has expected attributes in request', () => {
+ const query = ToggleCustomFieldConfigurationRt.decode(defaultRequest);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+
+ it('removes foo:bar attributes from request', () => {
+ const query = ToggleCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/cases/common/types/api/configure/v1.ts b/x-pack/plugins/cases/common/types/api/configure/v1.ts
index fffff5310f6af..8f98b760c9186 100644
--- a/x-pack/plugins/cases/common/types/api/configure/v1.ts
+++ b/x-pack/plugins/cases/common/types/api/configure/v1.ts
@@ -6,10 +6,74 @@
*/
import * as rt from 'io-ts';
+import {
+ MAX_CUSTOM_FIELDS_PER_CASE,
+ MAX_CUSTOM_FIELD_KEY_LENGTH,
+ MAX_CUSTOM_FIELD_LABEL_LENGTH,
+} from '../../../constants';
+import { limitedArraySchema, limitedStringSchema, regexStringRt } from '../../../schema';
+import { CustomFieldTextTypeRt, CustomFieldToggleTypeRt } from '../../domain';
import type { Configurations, Configuration } from '../../domain/configure/v1';
-import { ConfigurationBasicWithoutOwnerRt, CasesConfigureBasicRt } from '../../domain/configure/v1';
+import { ConfigurationBasicWithoutOwnerRt, ClosureTypeRt } from '../../domain/configure/v1';
+import { CaseConnectorRt } from '../../domain/connector/v1';
-export const ConfigurationRequestRt = CasesConfigureBasicRt;
+export const CustomFieldConfigurationWithoutTypeRt = rt.strict({
+ /**
+ * key of custom field
+ */
+ key: regexStringRt({
+ codec: limitedStringSchema({ fieldName: 'key', min: 1, max: MAX_CUSTOM_FIELD_KEY_LENGTH }),
+ pattern: '^[a-z0-9_-]+$',
+ message: `Key must be lower case, a-z, 0-9, '_', and '-' are allowed`,
+ }),
+ /**
+ * label of custom field
+ */
+ label: limitedStringSchema({ fieldName: 'label', min: 1, max: MAX_CUSTOM_FIELD_LABEL_LENGTH }),
+ /**
+ * custom field options - required
+ */
+ required: rt.boolean,
+});
+
+export const TextCustomFieldConfigurationRt = rt.intersection([
+ rt.strict({ type: CustomFieldTextTypeRt }),
+ CustomFieldConfigurationWithoutTypeRt,
+]);
+
+export const ToggleCustomFieldConfigurationRt = rt.intersection([
+ rt.strict({ type: CustomFieldToggleTypeRt }),
+ CustomFieldConfigurationWithoutTypeRt,
+]);
+
+export const CustomFieldsConfigurationRt = limitedArraySchema({
+ codec: rt.union([TextCustomFieldConfigurationRt, ToggleCustomFieldConfigurationRt]),
+ min: 0,
+ max: MAX_CUSTOM_FIELDS_PER_CASE,
+ fieldName: 'customFields',
+});
+
+export const ConfigurationRequestRt = rt.intersection([
+ rt.strict({
+ /**
+ * The external connector
+ */
+ connector: CaseConnectorRt,
+ /**
+ * Whether to close the case after it has been synced with the external system
+ */
+ closure_type: ClosureTypeRt,
+ /**
+ * The plugin owner that manages this configuration
+ */
+ owner: rt.string,
+ }),
+ rt.exact(
+ rt.partial({
+ customFields: CustomFieldsConfigurationRt,
+ })
+ ),
+]);
export const GetConfigurationFindRequestRt = rt.exact(
rt.partial({
@@ -26,7 +90,13 @@ export const CaseConfigureRequestParamsRt = rt.strict({
});
export const ConfigurationPatchRequestRt = rt.intersection([
- rt.exact(rt.partial(ConfigurationBasicWithoutOwnerRt.type.props)),
+ rt.exact(
+ rt.partial({
+ closure_type: ConfigurationBasicWithoutOwnerRt.type.props.closure_type,
+ connector: ConfigurationBasicWithoutOwnerRt.type.props.connector,
+ customFields: CustomFieldsConfigurationRt,
+ })
+ ),
rt.strict({ version: rt.string }),
]);
diff --git a/x-pack/plugins/cases/common/types/domain/case/v1.test.ts b/x-pack/plugins/cases/common/types/domain/case/v1.test.ts
index ba04082519c8c..d7dc74f6c3cb4 100644
--- a/x-pack/plugins/cases/common/types/domain/case/v1.test.ts
+++ b/x-pack/plugins/cases/common/types/domain/case/v1.test.ts
@@ -74,6 +74,18 @@ const basicCase = {
// damaged_raccoon uid
assignees: [{ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }],
category: null,
+ customFields: [
+ {
+ key: 'first_custom_field_key',
+ type: 'text',
+ value: ['this is a text field value', 'this is second'],
+ },
+ {
+ key: 'second_custom_field_key',
+ type: 'toggle',
+ value: true,
+ },
+ ],
};
describe('RelatedCaseRt', () => {
@@ -170,6 +182,18 @@ describe('CaseAttributesRt', () => {
updated_at: '2020-02-20T15:02:57.995Z',
updated_by: null,
category: null,
+ customFields: [
+ {
+ key: 'first_custom_field_key',
+ type: 'text',
+ value: ['this is a text field value', 'this is second'],
+ },
+ {
+ key: 'second_custom_field_key',
+ type: 'toggle',
+ value: true,
+ },
+ ],
};
it('has expected attributes in request', () => {
diff --git a/x-pack/plugins/cases/common/types/domain/case/v1.ts b/x-pack/plugins/cases/common/types/domain/case/v1.ts
index e4e16b7fb33a7..d8da843e46a0c 100644
--- a/x-pack/plugins/cases/common/types/domain/case/v1.ts
+++ b/x-pack/plugins/cases/common/types/domain/case/v1.ts
@@ -11,6 +11,7 @@ import { ExternalServiceRt } from '../external_service/v1';
import { CaseAssigneesRt, UserRt } from '../user/v1';
import { CaseConnectorRt } from '../connector/v1';
import { AttachmentRt } from '../attachment/v1';
+import { CaseCustomFieldsRt } from '../custom_field/v1';
export { CaseStatuses };
@@ -92,6 +93,11 @@ const CaseBasicRt = rt.strict({
* The category of the case.
*/
category: rt.union([rt.string, rt.null]),
+ /**
+ * An array containing the possible,
+ * user-configured custom fields.
+ */
+ customFields: CaseCustomFieldsRt,
});
export const CaseAttributesRt = rt.intersection([
diff --git a/x-pack/plugins/cases/common/types/domain/configure/v1.test.ts b/x-pack/plugins/cases/common/types/domain/configure/v1.test.ts
index 70b181ef004e6..9af0d2b474b76 100644
--- a/x-pack/plugins/cases/common/types/domain/configure/v1.test.ts
+++ b/x-pack/plugins/cases/common/types/domain/configure/v1.test.ts
@@ -6,7 +6,14 @@
*/
import { ConnectorTypes } from '../connector/v1';
-import { ConfigurationAttributesRt, ConfigurationRt } from './v1';
+import { CustomFieldTypes } from '../custom_field/v1';
+import {
+ ConfigurationAttributesRt,
+ ConfigurationRt,
+ CustomFieldConfigurationWithoutTypeRt,
+ TextCustomFieldConfigurationRt,
+ ToggleCustomFieldConfigurationRt,
+} from './v1';
describe('configure', () => {
const serviceNow = {
@@ -23,10 +30,25 @@ describe('configure', () => {
fields: null,
};
+ const textCustomField = {
+ key: 'text_custom_field',
+ label: 'Text custom field',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ };
+
+ const toggleCustomField = {
+ key: 'toggle_custom_field',
+ label: 'Toggle custom field',
+ type: CustomFieldTypes.TOGGLE,
+ required: false,
+ };
+
describe('ConfigurationAttributesRt', () => {
const defaultRequest = {
connector: resilient,
closure_type: 'close-by-user',
+ customFields: [textCustomField, toggleCustomField],
owner: 'cases',
created_at: '2020-02-19T23:06:33.798Z',
created_by: {
@@ -47,7 +69,10 @@ describe('configure', () => {
expect(query).toStrictEqual({
_tag: 'Right',
- right: defaultRequest,
+ right: {
+ ...defaultRequest,
+ customFields: [textCustomField, toggleCustomField],
+ },
});
});
@@ -56,7 +81,25 @@ describe('configure', () => {
expect(query).toStrictEqual({
_tag: 'Right',
- right: defaultRequest,
+ right: {
+ ...defaultRequest,
+ customFields: [textCustomField, toggleCustomField],
+ },
+ });
+ });
+
+ it('removes foo:bar attributes from custom fields', () => {
+ const query = ConfigurationAttributesRt.decode({
+ ...defaultRequest,
+ customFields: [{ ...textCustomField, foo: 'bar' }, toggleCustomField],
+ });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: {
+ ...defaultRequest,
+ customFields: [textCustomField, toggleCustomField],
+ },
});
});
});
@@ -65,6 +108,7 @@ describe('configure', () => {
const defaultRequest = {
connector: serviceNow,
closure_type: 'close-by-user',
+ customFields: [],
created_at: '2020-02-19T23:06:33.798Z',
created_by: {
full_name: 'Leslie Knope',
@@ -116,4 +160,84 @@ describe('configure', () => {
});
});
});
+
+ describe('CustomFieldConfigurationWithoutTypeRt', () => {
+ const defaultRequest = {
+ key: 'custom_field_key',
+ label: 'Custom field label',
+ required: false,
+ };
+
+ it('has expected attributes in request', () => {
+ const query = CustomFieldConfigurationWithoutTypeRt.decode(defaultRequest);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+
+ it('removes foo:bar attributes from request', () => {
+ const query = CustomFieldConfigurationWithoutTypeRt.decode({ ...defaultRequest, foo: 'bar' });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+ });
+
+ describe('TextCustomFieldConfigurationRt', () => {
+ const defaultRequest = {
+ key: 'my_text_custom_field',
+ label: 'Text Custom Field',
+ type: CustomFieldTypes.TEXT,
+ required: true,
+ };
+
+ it('has expected attributes in request', () => {
+ const query = TextCustomFieldConfigurationRt.decode(defaultRequest);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+
+ it('removes foo:bar attributes from request', () => {
+ const query = TextCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+ });
+
+ describe('ToggleCustomFieldConfigurationRt', () => {
+ const defaultRequest = {
+ key: 'my_toggle_custom_field',
+ label: 'Toggle Custom Field',
+ type: CustomFieldTypes.TOGGLE,
+ required: false,
+ };
+
+ it('has expected attributes in request', () => {
+ const query = ToggleCustomFieldConfigurationRt.decode(defaultRequest);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+
+ it('removes foo:bar attributes from request', () => {
+ const query = ToggleCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: { ...defaultRequest },
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/cases/common/types/domain/configure/v1.ts b/x-pack/plugins/cases/common/types/domain/configure/v1.ts
index 664793c23b198..56a144c248c2a 100644
--- a/x-pack/plugins/cases/common/types/domain/configure/v1.ts
+++ b/x-pack/plugins/cases/common/types/domain/configure/v1.ts
@@ -8,8 +8,44 @@
import * as rt from 'io-ts';
import { CaseConnectorRt, ConnectorMappingsRt } from '../connector/v1';
import { UserRt } from '../user/v1';
+import { CustomFieldTextTypeRt, CustomFieldToggleTypeRt } from '../custom_field/v1';
-const ClosureTypeRt = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]);
+export const ClosureTypeRt = rt.union([
+ rt.literal('close-by-user'),
+ rt.literal('close-by-pushing'),
+]);
+
+export const CustomFieldConfigurationWithoutTypeRt = rt.strict({
+ /**
+ * key of custom field
+ */
+ key: rt.string,
+ /**
+ * label of custom field
+ */
+ label: rt.string,
+ /**
+ * custom field options - required
+ */
+ required: rt.boolean,
+});
+
+export const TextCustomFieldConfigurationRt = rt.intersection([
+ rt.strict({ type: CustomFieldTextTypeRt }),
+ CustomFieldConfigurationWithoutTypeRt,
+]);
+
+export const ToggleCustomFieldConfigurationRt = rt.intersection([
+ rt.strict({ type: CustomFieldToggleTypeRt }),
+ CustomFieldConfigurationWithoutTypeRt,
+]);
+
+export const CustomFieldConfigurationRt = rt.union([
+ TextCustomFieldConfigurationRt,
+ ToggleCustomFieldConfigurationRt,
+]);
+
+export const CustomFieldsConfigurationRt = rt.array(CustomFieldConfigurationRt);
export const ConfigurationBasicWithoutOwnerRt = rt.strict({
/**
@@ -20,6 +56,10 @@ export const ConfigurationBasicWithoutOwnerRt = rt.strict({
* Whether to close the case after it has been synced with the external system
*/
closure_type: ClosureTypeRt,
+ /**
+ * The custom fields configured for the case
+ */
+ customFields: CustomFieldsConfigurationRt,
});
export const CasesConfigureBasicRt = rt.intersection([
@@ -57,6 +97,8 @@ export const ConfigurationRt = rt.intersection([
export const ConfigurationsRt = rt.array(ConfigurationRt);
+export type CustomFieldsConfiguration = rt.TypeOf;
+export type CustomFieldConfiguration = rt.TypeOf;
export type ClosureType = rt.TypeOf;
export type ConfigurationAttributes = rt.TypeOf;
export type Configuration = rt.TypeOf;
diff --git a/x-pack/plugins/cases/common/types/domain/custom_field/latest.ts b/x-pack/plugins/cases/common/types/domain/custom_field/latest.ts
new file mode 100644
index 0000000000000..25300c97a6d2e
--- /dev/null
+++ b/x-pack/plugins/cases/common/types/domain/custom_field/latest.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './v1';
diff --git a/x-pack/plugins/cases/common/types/domain/custom_field/v1.test.ts b/x-pack/plugins/cases/common/types/domain/custom_field/v1.test.ts
new file mode 100644
index 0000000000000..2100b221239a9
--- /dev/null
+++ b/x-pack/plugins/cases/common/types/domain/custom_field/v1.test.ts
@@ -0,0 +1,73 @@
+/*
+ * 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 { PathReporter } from 'io-ts/lib/PathReporter';
+import { CaseCustomFieldRt } from './v1';
+
+describe('CaseCustomFieldRt', () => {
+ it.each([
+ [
+ 'type text value text',
+ {
+ key: 'string_custom_field_1',
+ type: 'text',
+ value: ['this is a text field value'],
+ },
+ ],
+ [
+ 'type text value null',
+ {
+ key: 'string_custom_field_2',
+ type: 'text',
+ value: null,
+ },
+ ],
+ [
+ 'type toggle value boolean',
+ {
+ key: 'toggle_custom_field_1',
+ type: 'toggle',
+ value: true,
+ },
+ ],
+ [
+ 'type toggle value null',
+ {
+ key: 'toggle_custom_field_2',
+ type: 'toggle',
+ value: null,
+ },
+ ],
+ ])(`has expected attributes for customField with %s`, (_, customField) => {
+ const query = CaseCustomFieldRt.decode(customField);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: customField,
+ });
+ });
+
+ it('fails if text type and value do not match expected attributes in request', () => {
+ const query = CaseCustomFieldRt.decode({
+ key: 'text_custom_field_1',
+ type: 'text',
+ value: [1],
+ });
+
+ expect(PathReporter.report(query)[0]).toContain('Invalid value 1 supplied');
+ });
+
+ it('fails if toggle type and value do not match expected attributes in request', () => {
+ const query = CaseCustomFieldRt.decode({
+ key: 'list_custom_field_1',
+ type: 'toggle',
+ value: 'hello',
+ });
+
+ expect(PathReporter.report(query)[0]).toContain('Invalid value "hello" supplied');
+ });
+});
diff --git a/x-pack/plugins/cases/common/types/domain/custom_field/v1.ts b/x-pack/plugins/cases/common/types/domain/custom_field/v1.ts
new file mode 100644
index 0000000000000..44a8a01740ea9
--- /dev/null
+++ b/x-pack/plugins/cases/common/types/domain/custom_field/v1.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import * as rt from 'io-ts';
+
+export enum CustomFieldTypes {
+ TEXT = 'text',
+ TOGGLE = 'toggle',
+}
+
+export const CustomFieldTextTypeRt = rt.literal(CustomFieldTypes.TEXT);
+export const CustomFieldToggleTypeRt = rt.literal(CustomFieldTypes.TOGGLE);
+
+const CaseCustomFieldTextRt = rt.strict({
+ key: rt.string,
+ type: CustomFieldTextTypeRt,
+ value: rt.union([rt.array(rt.string), rt.null]),
+});
+
+export const CaseCustomFieldToggleRt = rt.strict({
+ key: rt.string,
+ type: CustomFieldToggleTypeRt,
+ value: rt.union([rt.boolean, rt.null]),
+});
+
+export const CaseCustomFieldRt = rt.union([CaseCustomFieldTextRt, CaseCustomFieldToggleRt]);
+export const CaseCustomFieldsRt = rt.array(CaseCustomFieldRt);
+
+export type CaseCustomFields = rt.TypeOf;
+export type CaseCustomField = rt.TypeOf;
+export type CaseCustomFieldToggle = rt.TypeOf;
+export type CaseCustomFieldText = rt.TypeOf;
diff --git a/x-pack/plugins/cases/common/types/domain/index.ts b/x-pack/plugins/cases/common/types/domain/index.ts
index c46b34d182877..ef317908b4627 100644
--- a/x-pack/plugins/cases/common/types/domain/index.ts
+++ b/x-pack/plugins/cases/common/types/domain/index.ts
@@ -7,6 +7,7 @@
// Latest
export * from './configure/latest';
+export * from './custom_field/latest';
export * from './user_action/latest';
export * from './external_service/latest';
export * from './case/latest';
@@ -16,6 +17,7 @@ export * from './attachment/latest';
// V1
export * as configureDomainV1 from './configure/v1';
+export * as customFieldDomainV1 from './custom_field/v1';
export * as userActionDomainV1 from './user_action/v1';
export * as externalServiceDomainV1 from './external_service/v1';
export * as caseDomainV1 from './case/v1';
diff --git a/x-pack/plugins/cases/common/types/domain/user_action/action/v1.ts b/x-pack/plugins/cases/common/types/domain/user_action/action/v1.ts
index 3cdb80ed534d1..22c51e1da4d10 100644
--- a/x-pack/plugins/cases/common/types/domain/user_action/action/v1.ts
+++ b/x-pack/plugins/cases/common/types/domain/user_action/action/v1.ts
@@ -25,6 +25,7 @@ export const UserActionTypes = {
create_case: 'create_case',
delete_case: 'delete_case',
category: 'category',
+ customFields: 'customFields',
} as const;
type UserActionActionTypeKeys = keyof typeof UserActionTypes;
diff --git a/x-pack/plugins/cases/common/types/domain/user_action/create_case/v1.test.ts b/x-pack/plugins/cases/common/types/domain/user_action/create_case/v1.test.ts
index 17e4777d18afa..b0e0ce59945be 100644
--- a/x-pack/plugins/cases/common/types/domain/user_action/create_case/v1.test.ts
+++ b/x-pack/plugins/cases/common/types/domain/user_action/create_case/v1.test.ts
@@ -74,6 +74,33 @@ describe('Create case', () => {
});
});
+ it('customFields are decoded correctly', () => {
+ const customFields = [
+ {
+ key: 'first_custom_field_key',
+ type: 'text',
+ value: ['this is a text field value', 'this is second'],
+ },
+ {
+ key: 'second_custom_field_key',
+ type: 'toggle',
+ value: true,
+ },
+ ];
+
+ const defaultRequestWithCustomFields = {
+ ...defaultRequest,
+ payload: { ...defaultRequest.payload, customFields },
+ };
+
+ const query = CreateCaseUserActionRt.decode(defaultRequestWithCustomFields);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: defaultRequestWithCustomFields,
+ });
+ });
+
it('removes foo:bar attributes from request', () => {
const query = CreateCaseUserActionRt.decode({ ...defaultRequest, foo: 'bar' });
@@ -159,6 +186,33 @@ describe('Create case', () => {
});
});
+ it('customFields are decoded correctly', () => {
+ const customFields = [
+ {
+ key: 'first_custom_field_key',
+ type: 'text',
+ value: ['this is a text field value', 'this is second'],
+ },
+ {
+ key: 'second_custom_field_key',
+ type: 'toggle',
+ value: true,
+ },
+ ];
+
+ const defaultRequestWithCustomFields = {
+ ...defaultRequest,
+ payload: { ...defaultRequest.payload, customFields },
+ };
+
+ const query = CreateCaseUserActionWithoutConnectorIdRt.decode(defaultRequestWithCustomFields);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: defaultRequestWithCustomFields,
+ });
+ });
+
it('removes foo:bar attributes from request', () => {
const query = CreateCaseUserActionWithoutConnectorIdRt.decode({
...defaultRequest,
diff --git a/x-pack/plugins/cases/common/types/domain/user_action/create_case/v1.ts b/x-pack/plugins/cases/common/types/domain/user_action/create_case/v1.ts
index 51eba91624b85..0a0938c9e4b2a 100644
--- a/x-pack/plugins/cases/common/types/domain/user_action/create_case/v1.ts
+++ b/x-pack/plugins/cases/common/types/domain/user_action/create_case/v1.ts
@@ -13,6 +13,7 @@ import {
ConnectorUserActionPayloadRt,
ConnectorUserActionPayloadWithoutConnectorIdRt,
} from '../connector/v1';
+import { CustomFieldsUserActionPayloadRt } from '../custom_fields/v1';
import { DescriptionUserActionPayloadRt } from '../description/v1';
import { SettingsUserActionPayloadRt } from '../settings/v1';
import { TagsUserActionPayloadRt } from '../tags/v1';
@@ -36,6 +37,7 @@ const CommonPayloadAttributesRt = rt.strict({
const OptionalPayloadAttributesRt = rt.exact(
rt.partial({
category: CategoryUserActionPayloadRt.type.props.category,
+ customFields: CustomFieldsUserActionPayloadRt.type.props.customFields,
})
);
diff --git a/x-pack/plugins/cases/common/types/domain/user_action/custom_fields/latest.ts b/x-pack/plugins/cases/common/types/domain/user_action/custom_fields/latest.ts
new file mode 100644
index 0000000000000..25300c97a6d2e
--- /dev/null
+++ b/x-pack/plugins/cases/common/types/domain/user_action/custom_fields/latest.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './v1';
diff --git a/x-pack/plugins/cases/common/types/domain/user_action/custom_fields/v1.test.ts b/x-pack/plugins/cases/common/types/domain/user_action/custom_fields/v1.test.ts
new file mode 100644
index 0000000000000..39d3f9a5f08c1
--- /dev/null
+++ b/x-pack/plugins/cases/common/types/domain/user_action/custom_fields/v1.test.ts
@@ -0,0 +1,101 @@
+/*
+ * 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 { UserActionTypes } from '../action/v1';
+import { CustomFieldsUserActionPayloadRt, CustomFieldsUserActionRt } from './v1';
+
+describe('Custom field', () => {
+ describe('CustomFieldsUserActionPayloadRt', () => {
+ const defaultRequest = {
+ customFields: [
+ {
+ key: 'first_custom_field_key',
+ type: 'text',
+ value: ['this is a text field value'],
+ },
+ ],
+ };
+
+ it('has expected attributes in request', () => {
+ const query = CustomFieldsUserActionPayloadRt.decode(defaultRequest);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: defaultRequest,
+ });
+ });
+
+ it('removes foo:bar attributes from request', () => {
+ const query = CustomFieldsUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: defaultRequest,
+ });
+ });
+ });
+
+ describe('CustomFieldsUserActionRt', () => {
+ const defaultRequest = {
+ type: UserActionTypes.customFields,
+ payload: {
+ customFields: [
+ {
+ key: 'first_custom_field_key',
+ type: 'text',
+ value: ['this is a text field value'],
+ },
+ ],
+ },
+ };
+
+ it('has expected attributes in request', () => {
+ const query = CustomFieldsUserActionRt.decode(defaultRequest);
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: defaultRequest,
+ });
+ });
+
+ it('removes foo:bar attributes from request', () => {
+ const query = CustomFieldsUserActionRt.decode({ ...defaultRequest, foo: 'bar' });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: defaultRequest,
+ });
+ });
+
+ it('removes foo:bar attributes from payload', () => {
+ const query = CustomFieldsUserActionRt.decode({
+ ...defaultRequest,
+ payload: { ...defaultRequest.payload, foo: 'bar' },
+ });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: defaultRequest,
+ });
+ });
+
+ it('removes foo:bar attributes from the field', () => {
+ const query = CustomFieldsUserActionRt.decode({
+ ...defaultRequest,
+ payload: {
+ ...defaultRequest.payload,
+ customFields: [{ ...defaultRequest.payload.customFields[0], foo: 'bar' }],
+ },
+ });
+
+ expect(query).toStrictEqual({
+ _tag: 'Right',
+ right: defaultRequest,
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/cases/common/types/domain/user_action/custom_fields/v1.ts b/x-pack/plugins/cases/common/types/domain/user_action/custom_fields/v1.ts
new file mode 100644
index 0000000000000..35a0377e03d60
--- /dev/null
+++ b/x-pack/plugins/cases/common/types/domain/user_action/custom_fields/v1.ts
@@ -0,0 +1,19 @@
+/*
+ * 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 * as rt from 'io-ts';
+import { CaseCustomFieldsRt } from '../../custom_field/v1';
+import { UserActionTypes } from '../action/v1';
+
+export const CustomFieldsUserActionPayloadRt = rt.strict({
+ customFields: CaseCustomFieldsRt,
+});
+
+export const CustomFieldsUserActionRt = rt.strict({
+ type: rt.literal(UserActionTypes.customFields),
+ payload: CustomFieldsUserActionPayloadRt,
+});
diff --git a/x-pack/plugins/cases/common/types/domain/user_action/v1.ts b/x-pack/plugins/cases/common/types/domain/user_action/v1.ts
index 365e21e305996..fdef2a9530e54 100644
--- a/x-pack/plugins/cases/common/types/domain/user_action/v1.ts
+++ b/x-pack/plugins/cases/common/types/domain/user_action/v1.ts
@@ -22,6 +22,7 @@ import { SeverityUserActionRt } from './severity/v1';
import { StatusUserActionRt } from './status/v1';
import { TagsUserActionRt } from './tags/v1';
import { TitleUserActionRt } from './title/v1';
+import { CustomFieldsUserActionRt } from './custom_fields/v1';
export { UserActionTypes, UserActionActions } from './action/v1';
export { StatusUserActionRt } from './status/v1';
@@ -59,6 +60,7 @@ const BasicUserActionsRt = rt.union([
AssigneesUserActionRt,
DeleteCaseUserActionRt,
CategoryUserActionRt,
+ CustomFieldsUserActionRt,
]);
const CommonUserActionsWithIdsRt = rt.union([BasicUserActionsRt, CommentUserActionRt]);
@@ -151,3 +153,4 @@ export type CreateCaseUserAction = UserAction
>;
+export type CustomFieldsUserAction = UserAction>;
diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts
index e2e453aaaf278..2a76e56a59fe0 100644
--- a/x-pack/plugins/cases/common/ui/types.ts
+++ b/x-pack/plugins/cases/common/ui/types.ts
@@ -25,6 +25,7 @@ import type {
Attachment,
ExternalReferenceAttachment,
PersistableStateAttachment,
+ Configuration,
} from '../types/domain';
import type {
CasePatchRequest,
@@ -110,6 +111,7 @@ export type CasesMetrics = SnakeToCamelCase;
export type CaseUpdateRequest = SnakeToCamelCase;
export type CaseConnectors = SnakeToCamelCase;
export type CaseUsers = GetCaseUsersResponse;
+export type CaseUICustomField = CaseUI['customFields'][number];
export interface ResolvedCase {
case: CaseUI;
@@ -118,6 +120,13 @@ export interface ResolvedCase {
aliasPurpose?: ResolvedSimpleSavedObject['alias_purpose'];
}
+export type CasesConfigurationUI = Pick<
+ SnakeToCamelCase,
+ 'closureType' | 'connector' | 'mappings' | 'customFields' | 'id' | 'version'
+>;
+
+export type CasesConfigurationUICustomField = CasesConfigurationUI['customFields'][number];
+
export type SortOrder = 'asc' | 'desc';
export const SORT_ORDER_VALUES: SortOrder[] = ['asc', 'desc'];
@@ -205,6 +214,7 @@ export type UpdateKey = keyof Pick<
| 'severity'
| 'assignees'
| 'category'
+ | 'customFields'
>;
export interface UpdateByKey {
diff --git a/x-pack/plugins/cases/docs/openapi/bundled.json b/x-pack/plugins/cases/docs/openapi/bundled.json
index c3fef89e021dd..b36a745179833 100644
--- a/x-pack/plugins/cases/docs/openapi/bundled.json
+++ b/x-pack/plugins/cases/docs/openapi/bundled.json
@@ -4579,6 +4579,54 @@
],
"default": "low"
},
+ "custom_fields_property": {
+ "type": "array",
+ "description": "Values for custom fields of a case",
+ "minItems": 0,
+ "maxItems": 5,
+ "items": {
+ "type": "object",
+ "required": [
+ "key",
+ "type",
+ "field"
+ ],
+ "properties": {
+ "key": {
+ "description": "The key identifying the custom field.",
+ "type": "string"
+ },
+ "type": {
+ "description": "The type of the custom field. Should match the key and how the custom field was configured",
+ "type": "string"
+ },
+ "field": {
+ "description": "An object containing the value of the field.",
+ "type": "object",
+ "required": [
+ "value"
+ ],
+ "properties": {
+ "value": {
+ "description": "The value of the field.",
+ "nullable": true,
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "boolean"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"create_case_request": {
"title": "Create case request",
"description": "The create case API request body varies depending on the type of connector.",
@@ -4652,6 +4700,9 @@
"description": "A title for the case.",
"type": "string",
"maxLength": 160
+ },
+ "customFields": {
+ "$ref": "#/components/schemas/custom_fields_property"
}
}
},
@@ -5296,6 +5347,9 @@
"version": {
"description": "The current version of the case. To determine this value, use the get case or find cases APIs.",
"type": "string"
+ },
+ "customFields": {
+ "$ref": "#/components/schemas/custom_fields_property"
}
}
}
diff --git a/x-pack/plugins/cases/docs/openapi/bundled.yaml b/x-pack/plugins/cases/docs/openapi/bundled.yaml
index f818d279a2e27..ddee756120e01 100644
--- a/x-pack/plugins/cases/docs/openapi/bundled.yaml
+++ b/x-pack/plugins/cases/docs/openapi/bundled.yaml
@@ -2890,6 +2890,38 @@ components:
- low
- medium
default: low
+ custom_fields_property:
+ type: array
+ description: Values for custom fields of a case
+ minItems: 0
+ maxItems: 5
+ items:
+ type: object
+ required:
+ - key
+ - type
+ - field
+ properties:
+ key:
+ description: The key identifying the custom field.
+ type: string
+ type:
+ description: The type of the custom field. Should match the key and how the custom field was configured
+ type: string
+ field:
+ description: An object containing the value of the field.
+ type: object
+ required:
+ - value
+ properties:
+ value:
+ description: The value of the field.
+ nullable: true
+ type: array
+ items:
+ anyOf:
+ - type: string
+ - type: boolean
create_case_request:
title: Create case request
description: The create case API request body varies depending on the type of connector.
@@ -2938,6 +2970,8 @@ components:
description: A title for the case.
type: string
maxLength: 160
+ customFields:
+ $ref: '#/components/schemas/custom_fields_property'
case_response_closed_by_properties:
title: Case response properties for closed_by
type: object
@@ -3403,6 +3437,8 @@ components:
version:
description: The current version of the case. To determine this value, use the get case or find cases APIs.
type: string
+ customFields:
+ $ref: '#/components/schemas/custom_fields_property'
searchFieldsType:
type: string
description: The fields to perform the `simple_query_string` parsed query against.
diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/create_case_request.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/create_case_request.yaml
index 1092975985dd0..7b3d8d8219ff7 100644
--- a/x-pack/plugins/cases/docs/openapi/components/schemas/create_case_request.yaml
+++ b/x-pack/plugins/cases/docs/openapi/components/schemas/create_case_request.yaml
@@ -3,12 +3,12 @@ description: >-
The create case API request body varies depending on the type of connector.
type: object
required:
- - connector
- - description
- - owner
- - settings
- - tags
- - title
+ - connector
+ - description
+ - owner
+ - settings
+ - tags
+ - title
properties:
assignees:
$ref: 'assignees.yaml'
@@ -45,4 +45,6 @@ properties:
title:
description: A title for the case.
type: string
- maxLength: 160
\ No newline at end of file
+ maxLength: 160
+ customFields:
+ $ref: 'custom_fields_property.yaml'
diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/custom_fields_property.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/custom_fields_property.yaml
new file mode 100644
index 0000000000000..2b682960bd1f0
--- /dev/null
+++ b/x-pack/plugins/cases/docs/openapi/components/schemas/custom_fields_property.yaml
@@ -0,0 +1,31 @@
+type: array
+description: Values for custom fields of a case
+minItems: 0
+maxItems: 5
+items:
+ type: object
+ required:
+ - key
+ - type
+ - field
+ properties:
+ key:
+ description: The key identifying the custom field.
+ type: string
+ type:
+ description: The type of the custom field. Should match the key and how the custom field was configured
+ type: string
+ field:
+ description: An object containing the value of the field.
+ type: object
+ required:
+ - value
+ properties:
+ value:
+ description: The value of the field.
+ nullable: true
+ type: array
+ items:
+ anyOf:
+ - type: string
+ - type: boolean
diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/update_case_request.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/update_case_request.yaml
index df3e7ab17d674..4bbac95630e47 100644
--- a/x-pack/plugins/cases/docs/openapi/components/schemas/update_case_request.yaml
+++ b/x-pack/plugins/cases/docs/openapi/components/schemas/update_case_request.yaml
@@ -58,3 +58,5 @@ properties:
version:
description: The current version of the case. To determine this value, use the get case or find cases APIs.
type: string
+ customFields:
+ $ref: 'custom_fields_property.yaml'
diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts
index 9dea7a3413f95..7295a08cfd51a 100644
--- a/x-pack/plugins/cases/public/common/translations.ts
+++ b/x-pack/plugins/cases/public/common/translations.ts
@@ -192,12 +192,8 @@ export const TITLE_REQUIRED = i18n.translate('xpack.cases.createCase.titleFieldR
defaultMessage: 'A name is required.',
});
-export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate('xpack.cases.configureCases.headerTitle', {
- defaultMessage: 'Configure cases',
-});
-
export const CONFIGURE_CASES_BUTTON = i18n.translate('xpack.cases.configureCasesButton', {
- defaultMessage: 'Edit external connection',
+ defaultMessage: 'Settings',
});
export const ADD_COMMENT = i18n.translate('xpack.cases.caseView.comment.addComment', {
@@ -380,3 +376,12 @@ export const ADD_TAG_CUSTOM_OPTION_LABEL_COMBO_BOX = ADD_TAG_CUSTOM_OPTION_LABEL
export const ADD_CATEGORY_CUSTOM_OPTION_LABEL_COMBO_BOX =
ADD_CATEGORY_CUSTOM_OPTION_LABEL('{searchValue}');
+
+export const EXPERIMENTAL_LABEL = i18n.translate('xpack.cases.badge.experimentalLabel', {
+ defaultMessage: 'Technical preview',
+});
+
+export const EXPERIMENTAL_DESC = i18n.translate('xpack.cases.badge.experimentalDesc', {
+ defaultMessage:
+ 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.',
+});
diff --git a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx
index 7661242fe9153..d34be81d499ff 100644
--- a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx
@@ -138,46 +138,6 @@ describe('AllCases', () => {
});
});
- it('should not allow the user to enter configuration page with basic license', async () => {
- useGetActionLicenseMock.mockReturnValue({
- ...defaultActionLicense,
- data: {
- id: '.jira',
- name: 'Jira',
- minimumLicenseRequired: 'gold',
- enabled: true,
- enabledInConfig: true,
- enabledInLicense: false,
- },
- });
-
- const result = appMockRender.render();
-
- await waitFor(() => {
- expect(result.getByTestId('configure-case-button')).toBeDisabled();
- });
- });
-
- it('should allow the user to enter configuration page with gold license and above', async () => {
- useGetActionLicenseMock.mockReturnValue({
- ...defaultActionLicense,
- data: {
- id: '.jira',
- name: 'Jira',
- minimumLicenseRequired: 'gold',
- enabled: true,
- enabledInConfig: true,
- enabledInLicense: true,
- },
- });
-
- const result = appMockRender.render();
-
- await waitFor(() => {
- expect(result.getByTestId('configure-case-button')).not.toBeDisabled();
- });
- });
-
it('should render the case callouts', async () => {
const result = appMockRender.render();
await waitFor(() => {
diff --git a/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx
index 59b26b921eae0..aafb287eed869 100644
--- a/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx
@@ -54,7 +54,6 @@ export const NavButtons: FunctionComponent = ({ actionsErrors }) => {
{actionsErrors[0].description}> : <>>}
titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''}
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 68c51f78c868a..0ba1f2214bfc0 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
@@ -10,13 +10,14 @@
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import { isEqual } from 'lodash';
+import { useGetCaseConfiguration } from '../../../containers/configure/use_get_case_configuration';
import { useGetCaseUsers } from '../../../containers/use_get_case_users';
import { useGetCaseConnectors } from '../../../containers/use_get_case_connectors';
import { useCasesFeatures } from '../../../common/use_cases_features';
import { useGetCurrentUserProfile } from '../../../containers/user_profiles/use_get_current_user_profile';
import { useGetSupportedActionConnectors } from '../../../containers/configure/use_get_supported_action_connectors';
import type { CaseSeverity, CaseStatuses } from '../../../../common/types/domain';
-import type { UseFetchAlertData } from '../../../../common/ui/types';
+import type { CaseUICustomField, UseFetchAlertData } from '../../../../common/ui/types';
import type { CaseUI } from '../../../../common';
import { EditConnector } from '../../edit_connector';
import type { CasesNavigation } from '../../links';
@@ -38,6 +39,7 @@ import { CaseViewTabs } from '../case_view_tabs';
import { Description } from '../../description';
import { EditCategory } from './edit_category';
import { parseCaseUsers } from '../../utils';
+import { CustomFields } from './custom_fields';
export const CaseViewActivity = ({
ruleDetailsNavigation,
@@ -72,6 +74,8 @@ export const CaseViewActivity = ({
const { data: caseUsers, isLoading: isLoadingCaseUsers } = useGetCaseUsers(caseData.id);
+ const { data: casesConfiguration } = useGetCaseConfiguration();
+
const { userProfiles, reporterAsArray } = parseCaseUsers({
caseUsers,
createdBy: caseData.createdBy,
@@ -148,6 +152,16 @@ export const CaseViewActivity = ({
[onUpdateField]
);
+ const onSubmitCustomFields = useCallback(
+ (customFields: CaseUICustomField[]) => {
+ onUpdateField({
+ key: 'customFields',
+ value: customFields,
+ });
+ },
+ [onUpdateField]
+ );
+
const handleUserActionsActivityChanged = useCallback(
(params: UserActivityParams) => {
setUserActivityQueryParams((oldParams) => ({
@@ -205,6 +219,7 @@ export const CaseViewActivity = ({
onRuleDetailsClick={ruleDetailsNavigation?.onClick}
caseConnectors={caseConnectors}
data={caseData}
+ casesConfiguration={casesConfiguration}
actionsNavigation={actionsNavigation}
onShowAlertDetails={onShowAlertDetails}
onUpdateField={onUpdateField}
@@ -283,6 +298,12 @@ export const CaseViewActivity = ({
key={caseData.connector.id}
/>
) : 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
new file mode 100644
index 0000000000000..ce532a41d64e9
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/case_view/components/custom_fields.test.tsx
@@ -0,0 +1,203 @@
+/*
+ * 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 { screen, waitFor, within } from '@testing-library/react';
+
+import type { AppMockRenderer } from '../../../common/mock';
+import { readCasesPermissions, createAppMockRenderer } from '../../../common/mock';
+
+import { CustomFields } from './custom_fields';
+import { customFieldsMock, customFieldsConfigurationMock } from '../../../containers/mock';
+import userEvent from '@testing-library/user-event';
+import { CustomFieldTypes } from '../../../../common/types/domain';
+
+describe('Case View Page files tab', () => {
+ const onSubmit = jest.fn();
+ let appMockRender: AppMockRenderer;
+
+ beforeEach(() => {
+ appMockRender = createAppMockRenderer();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render the custom fields correctly', async () => {
+ appMockRender.render(
+
+ );
+
+ expect(screen.getByTestId('case-custom-field-wrapper-test_key_1')).toBeInTheDocument();
+ expect(screen.getByTestId('case-custom-field-wrapper-test_key_2')).toBeInTheDocument();
+ });
+
+ it('should render the custom fields types when the custom fields are empty', async () => {
+ appMockRender.render(
+
+ );
+
+ expect(screen.getByTestId('case-custom-field-wrapper-test_key_1')).toBeInTheDocument();
+ expect(screen.getByTestId('case-custom-field-wrapper-test_key_2')).toBeInTheDocument();
+ });
+
+ it('should not show the custom fields if the configuration is empty', async () => {
+ appMockRender.render(
+
+ );
+
+ expect(screen.queryByTestId('case-custom-field-wrapper-test_key_1')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('case-custom-field-wrapper-test_key_2')).not.toBeInTheDocument();
+ });
+
+ it('should sort the custom fields correctly', async () => {
+ const reversedConfiguration = [...customFieldsConfigurationMock].reverse();
+
+ appMockRender.render(
+
+ );
+
+ const customFields = screen.getAllByTestId('case-custom-field-wrapper', { exact: false });
+
+ expect(customFields.length).toBe(2);
+
+ expect(within(customFields[0]).getByRole('heading')).toHaveTextContent('My test label 1');
+ expect(within(customFields[1]).getByRole('heading')).toHaveTextContent('My test label 2');
+ });
+
+ it('pass the permissions to custom fields correctly', async () => {
+ appMockRender = createAppMockRenderer({ permissions: readCasesPermissions() });
+
+ appMockRender.render(
+
+ );
+
+ expect(
+ screen.queryByTestId('case-text-custom-field-edit-button-test_key_1')
+ ).not.toBeInTheDocument();
+ });
+
+ it('adds missing custom fields with no custom fields in the case', async () => {
+ appMockRender.render(
+
+ );
+
+ userEvent.click(screen.getByRole('switch'));
+
+ await waitFor(() => {
+ expect(onSubmit).toBeCalledWith([
+ {
+ type: CustomFieldTypes.TEXT,
+ key: 'test_key_1',
+ value: null,
+ },
+ { type: CustomFieldTypes.TOGGLE, key: 'test_key_2', value: true },
+ ]);
+ });
+ });
+
+ it('adds missing custom fields with some custom fields in the case', async () => {
+ appMockRender.render(
+
+ );
+
+ userEvent.click(screen.getByRole('switch'));
+
+ await waitFor(() => {
+ expect(onSubmit).toBeCalledWith([
+ {
+ type: CustomFieldTypes.TEXT,
+ key: 'test_key_1',
+ value: null,
+ },
+ { type: CustomFieldTypes.TOGGLE, key: 'test_key_2', value: false },
+ ]);
+ });
+ });
+
+ it('removes extra custom fields', async () => {
+ appMockRender.render(
+
+ );
+
+ userEvent.click(screen.getByRole('switch'));
+
+ await waitFor(() => {
+ expect(onSubmit).toBeCalledWith([
+ { type: CustomFieldTypes.TOGGLE, key: 'test_key_2', value: false },
+ ]);
+ });
+ });
+
+ it('updates an existing field correctly', async () => {
+ appMockRender.render(
+
+ );
+
+ userEvent.click(screen.getByRole('switch'));
+
+ await waitFor(() => {
+ expect(onSubmit).toBeCalledWith([
+ customFieldsMock[0],
+ { type: CustomFieldTypes.TOGGLE, key: 'test_key_2', value: false },
+ ]);
+ });
+ });
+});
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
new file mode 100644
index 0000000000000..5d178c2709f62
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/case_view/components/custom_fields.tsx
@@ -0,0 +1,108 @@
+/*
+ * 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, { useCallback, useMemo } from 'react';
+import { sortBy } from 'lodash';
+import { EuiFlexItem } from '@elastic/eui';
+import type {
+ CasesConfigurationUI,
+ CasesConfigurationUICustomField,
+ 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;
+}
+
+const CustomFieldsComponent: React.FC = ({
+ isLoading,
+ customFields,
+ customFieldsConfiguration,
+ onSubmit,
+}) => {
+ const { permissions } = useCasesContext();
+ const sortedCustomFieldsConfiguration = useMemo(
+ () => sortCustomFieldsByLabel(customFieldsConfiguration),
+ [customFieldsConfiguration]
+ );
+
+ const onSubmitCustomField = useCallback(
+ (customFieldToAdd) => {
+ const allCustomFields = createMissingAndRemoveExtraCustomFields(
+ customFields,
+ customFieldsConfiguration
+ );
+
+ const updatedCustomFields = addOrReplaceCustomField(allCustomFields, customFieldToAdd);
+
+ onSubmit(updatedCustomFields);
+ },
+ [customFields, customFieldsConfiguration, onSubmit]
+ );
+
+ const customFieldsComponents = sortedCustomFieldsConfiguration.map((customFieldConf) => {
+ const customFieldFactory = customFieldsBuilderMap[customFieldConf.type];
+ const customFieldType = customFieldFactory().build();
+
+ const customField = customFields.find((field) => field.key === customFieldConf.key);
+
+ const EditComponent = customFieldType.Edit;
+
+ return (
+
+
+
+ );
+ });
+
+ return <>{customFieldsComponents}>;
+};
+
+CustomFieldsComponent.displayName = 'CustomFields';
+
+export const CustomFields = React.memo(CustomFieldsComponent);
+
+const sortCustomFieldsByLabel = (customFieldsConfiguration: Props['customFieldsConfiguration']) => {
+ return sortBy(customFieldsConfiguration, (customFieldConf) => {
+ 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
+ );
+
+ if (foundCustomField) {
+ return foundCustomField;
+ }
+
+ return { key: confCustomField.key, type: confCustomField.type, value: null };
+ });
+
+ return createdCustomFields;
+};
diff --git a/x-pack/plugins/cases/public/components/case_view/translations.ts b/x-pack/plugins/cases/public/components/case_view/translations.ts
index d0be88f729a69..d03b8426dac4a 100644
--- a/x-pack/plugins/cases/public/components/case_view/translations.ts
+++ b/x-pack/plugins/cases/public/components/case_view/translations.ts
@@ -49,6 +49,12 @@ export const REMOVED_FIELD = i18n.translate('xpack.cases.caseView.actionLabel.re
defaultMessage: 'removed',
});
+export const CHANGED_FIELD_TO_EMPTY = (field: string) =>
+ i18n.translate('xpack.cases.caseView.actionLabel.changeFieldToEmpty', {
+ values: { field },
+ defaultMessage: 'changed {field} to "None"',
+ });
+
export const VIEW_INCIDENT = (incidentNumber: string) =>
i18n.translate('xpack.cases.caseView.actionLabel.viewIncident', {
defaultMessage: 'View {incidentNumber}',
diff --git a/x-pack/plugins/cases/public/components/case_view/use_on_update_field.ts b/x-pack/plugins/cases/public/components/case_view/use_on_update_field.ts
index 03747ffd4c5fe..b2258549b242c 100644
--- a/x-pack/plugins/cases/public/components/case_view/use_on_update_field.ts
+++ b/x-pack/plugins/cases/public/components/case_view/use_on_update_field.ts
@@ -93,6 +93,12 @@ export const useOnUpdateField = ({ caseData }: { caseData: CaseUI }) => {
callUpdate('assignees', assigneesUpdate);
}
break;
+ case 'customFields':
+ const customFields = getTypedPayload(value);
+ if (!deepEqual(caseData.customFields, value)) {
+ callUpdate('customFields', customFields);
+ }
+ break;
default:
return null;
}
diff --git a/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx
index 28267d407cb3f..908d270d9cc1e 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx
@@ -8,7 +8,6 @@
import type { ActionTypeConnector } from '../../../../common/types/domain';
import { ConnectorTypes } from '../../../../common/types/domain';
import type { ActionConnector } from '../../../containers/configure/types';
-import type { ReturnUseCaseConfigure } from '../../../containers/configure/use_configure';
import { connectorsMock, actionTypesMock } from '../../../common/mock/connectors';
export { mappings } from '../../../containers/configure/mock';
@@ -18,35 +17,28 @@ export const actionTypes: ActionTypeConnector[] = actionTypesMock;
export const searchURL =
'?timerange=(global:(linkTo:!(),timerange:(from:1585487656371,fromStr:now-24h,kind:relative,to:1585574056371,toStr:now)),timeline:(linkTo:!(),timerange:(from:1585227005527,kind:absolute,to:1585313405527)))';
-export const useCaseConfigureResponse: ReturnUseCaseConfigure = {
- closureType: 'close-by-user',
- connector: {
- id: 'none',
- name: 'none',
- type: ConnectorTypes.none,
- fields: null,
- },
- currentConfiguration: {
+export const useCaseConfigureResponse = {
+ data: {
+ closureType: 'close-by-user',
connector: {
+ fields: null,
id: 'none',
name: 'none',
type: ConnectorTypes.none,
- fields: null,
},
- closureType: 'close-by-user',
+ customFields: [],
+ mappings: [],
+ version: '',
+ id: '',
},
- firstLoad: false,
- loading: false,
- mappings: [],
- persistCaseConfigure: jest.fn(),
- persistLoading: false,
- refetchCaseConfigure: jest.fn(),
- setClosureType: jest.fn(),
- setConnector: jest.fn(),
- setCurrentConfiguration: jest.fn(),
- setMappings: jest.fn(),
- version: '',
- id: '',
+ isLoading: false,
+ refetch: jest.fn(),
+};
+
+export const usePersistConfigurationMockResponse = {
+ isLoading: false,
+ mutate: jest.fn(),
+ mutateAsync: jest.fn(),
};
export const useConnectorsResponse = {
diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
index 5272eaebad512..8b54575292d84 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx
@@ -8,15 +8,19 @@
import React from 'react';
import type { ReactWrapper } from 'enzyme';
import { mount } from 'enzyme';
-import { waitFor } from '@testing-library/react';
+import { waitFor, screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import { ConfigureCases } from '.';
-import { noUpdateCasesPermissions, TestProviders } from '../../common/mock';
+import { noUpdateCasesPermissions, TestProviders, createAppMockRenderer } from '../../common/mock';
+import { customFieldsConfigurationMock } from '../../containers/mock';
+import type { AppMockRenderer } from '../../common/mock';
import { Connectors } from './connectors';
import { ClosureOptions } from './closure_options';
import { useKibana } from '../../common/lib/kibana';
-import { useCaseConfigure } from '../../containers/configure/use_configure';
+import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration';
+import { usePersistConfiguration } from '../../containers/configure/use_persist_configuration';
import {
connectors,
@@ -24,24 +28,31 @@ import {
useCaseConfigureResponse,
useConnectorsResponse,
useActionTypesResponse,
+ usePersistConfigurationMockResponse,
} from './__mock__';
-import { ConnectorTypes } from '../../../common/types/domain';
+import type { CustomFieldsConfiguration } from '../../../common/types/domain';
+import { ConnectorTypes, CustomFieldTypes } from '../../../common/types/domain';
import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock';
import { useGetActionTypes } from '../../containers/configure/use_action_types';
import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors';
+import { useLicense } from '../../common/use_license';
jest.mock('../../common/lib/kibana');
jest.mock('../../containers/configure/use_get_supported_action_connectors');
-jest.mock('../../containers/configure/use_configure');
+jest.mock('../../containers/configure/use_get_case_configuration');
+jest.mock('../../containers/configure/use_persist_configuration');
jest.mock('../../containers/configure/use_action_types');
+jest.mock('../../common/use_license');
const useKibanaMock = useKibana as jest.Mocked;
const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock;
-const useCaseConfigureMock = useCaseConfigure as jest.Mock;
+const useGetCaseConfigurationMock = useGetCaseConfiguration as jest.Mock;
+const usePersistConfigurationMock = usePersistConfiguration as jest.Mock;
const useGetUrlSearchMock = jest.fn();
const useGetActionTypesMock = useGetActionTypes as jest.Mock;
const getAddConnectorFlyoutMock = jest.fn();
const getEditConnectorFlyoutMock = jest.fn();
+const useLicenseMock = useLicense as jest.Mock;
describe('ConfigureCases', () => {
beforeAll(() => {
@@ -59,12 +70,14 @@ describe('ConfigureCases', () => {
beforeEach(() => {
useGetActionTypesMock.mockImplementation(() => useActionTypesResponse);
+ useLicenseMock.mockReturnValue({ isAtLeastGold: () => true });
});
describe('rendering', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
- useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse);
+ useGetCaseConfigurationMock.mockImplementation(() => useCaseConfigureResponse);
+ usePersistConfigurationMock.mockImplementation(() => usePersistConfigurationMockResponse);
useGetConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, data: [] }));
useGetUrlSearchMock.mockImplementation(() => searchURL);
@@ -100,25 +113,19 @@ describe('ConfigureCases', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
- useCaseConfigureMock.mockImplementation(() => ({
- ...useCaseConfigureResponse,
- closureType: 'close-by-user',
- connector: {
- id: 'not-id',
- name: 'unchanged',
- type: ConnectorTypes.none,
- fields: null,
- },
- currentConfiguration: {
+ useGetCaseConfigurationMock.mockImplementation(() => ({
+ data: {
+ ...useCaseConfigureResponse.data,
+ closureType: 'close-by-user',
connector: {
id: 'not-id',
name: 'unchanged',
type: ConnectorTypes.none,
fields: null,
},
- closureType: 'close-by-user',
},
}));
+
useGetConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, data: [] }));
useGetUrlSearchMock.mockImplementation(() => searchURL);
wrapper = mount(, {
@@ -145,26 +152,21 @@ describe('ConfigureCases', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
- useCaseConfigureMock.mockImplementation(() => ({
+ useGetCaseConfigurationMock.mockImplementation(() => ({
...useCaseConfigureResponse,
- mappings: [],
- closureType: 'close-by-user',
- connector: {
- id: 'servicenow-1',
- name: 'unchanged',
- type: ConnectorTypes.serviceNowITSM,
- fields: null,
- },
- currentConfiguration: {
+ data: {
+ ...useCaseConfigureResponse.data,
+ mappings: [],
+ closureType: 'close-by-user',
connector: {
id: 'servicenow-1',
name: 'unchanged',
type: ConnectorTypes.serviceNowITSM,
fields: null,
},
- closureType: 'close-by-user',
},
}));
+
useGetConnectorsMock.mockImplementation(() => useConnectorsResponse);
useGetUrlSearchMock.mockImplementation(() => searchURL);
@@ -226,24 +228,18 @@ describe('ConfigureCases', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
- useCaseConfigureMock.mockImplementation(() => ({
+ useGetCaseConfigurationMock.mockImplementation(() => ({
...useCaseConfigureResponse,
- mapping: null,
- closureType: 'close-by-user',
- connector: {
- id: 'resilient-2',
- name: 'unchanged',
- type: ConnectorTypes.resilient,
- fields: null,
- },
- currentConfiguration: {
+ data: {
+ ...useCaseConfigureResponse.data,
+ mapping: null,
+ closureType: 'close-by-user',
connector: {
- id: 'servicenow-1',
+ id: 'resilient-2',
name: 'unchanged',
- type: ConnectorTypes.serviceNowITSM,
+ type: ConnectorTypes.resilient,
fields: null,
},
- closureType: 'close-by-user',
},
}));
@@ -302,15 +298,22 @@ describe('ConfigureCases', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
- useCaseConfigureMock.mockImplementation(() => ({
+ useGetCaseConfigurationMock.mockImplementation(() => ({
...useCaseConfigureResponse,
- connector: {
- id: 'servicenow-1',
- name: 'SN',
- type: ConnectorTypes.serviceNowITSM,
- fields: null,
+ data: {
+ ...useCaseConfigureResponse.data,
+ connector: {
+ id: 'servicenow-1',
+ name: 'SN',
+ type: ConnectorTypes.serviceNowITSM,
+ fields: null,
+ },
},
- persistLoading: true,
+ }));
+
+ usePersistConfigurationMock.mockImplementation(() => ({
+ ...usePersistConfigurationMockResponse,
+ isLoading: true,
}));
useGetConnectorsMock.mockImplementation(() => useConnectorsResponse);
@@ -350,13 +353,15 @@ describe('ConfigureCases', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
- useCaseConfigureMock.mockImplementation(() => ({
+ useGetCaseConfigurationMock.mockImplementation(() => ({
...useCaseConfigureResponse,
- loading: true,
+ isLoading: true,
}));
+
useGetConnectorsMock.mockImplementation(() => ({
...useConnectorsResponse,
}));
+
useGetUrlSearchMock.mockImplementation(() => searchURL);
wrapper = mount(, {
wrappingComponent: TestProviders,
@@ -373,32 +378,29 @@ describe('ConfigureCases', () => {
});
describe('connectors', () => {
+ const persistCaseConfigure = jest.fn();
let wrapper: ReactWrapper;
- let persistCaseConfigure: jest.Mock;
beforeEach(() => {
- persistCaseConfigure = jest.fn();
- useCaseConfigureMock.mockImplementation(() => ({
- ...useCaseConfigureResponse,
- mapping: null,
- closureType: 'close-by-user',
- connector: {
- id: 'resilient-2',
- name: 'My connector',
- type: ConnectorTypes.resilient,
- fields: null,
- },
- currentConfiguration: {
+ useGetCaseConfigurationMock.mockImplementation(() => ({
+ data: {
+ ...useCaseConfigureResponse.data,
+ mapping: null,
+ closureType: 'close-by-user',
connector: {
- id: 'My connector',
+ id: 'resilient-2',
name: 'My connector',
- type: ConnectorTypes.jira,
+ type: ConnectorTypes.resilient,
fields: null,
},
- closureType: 'close-by-user',
},
- persistCaseConfigure,
}));
+
+ usePersistConfigurationMock.mockImplementation(() => ({
+ ...usePersistConfigurationMockResponse,
+ mutate: persistCaseConfigure,
+ }));
+
useGetConnectorsMock.mockImplementation(() => useConnectorsResponse);
useGetUrlSearchMock.mockImplementation(() => searchURL);
@@ -422,27 +424,36 @@ describe('ConfigureCases', () => {
fields: null,
},
closureType: 'close-by-user',
+ customFields: [],
+ id: '',
+ version: '',
});
});
test('the text of the update button is changed successfully', () => {
- useCaseConfigureMock
+ useGetCaseConfigurationMock
.mockImplementationOnce(() => ({
...useCaseConfigureResponse,
- connector: {
- id: 'servicenow-1',
- name: 'My connector',
- type: ConnectorTypes.serviceNowITSM,
- fields: null,
+ data: {
+ ...useCaseConfigureResponse.data,
+ connector: {
+ id: 'servicenow-1',
+ name: 'My connector',
+ type: ConnectorTypes.serviceNowITSM,
+ fields: null,
+ },
},
}))
.mockImplementation(() => ({
...useCaseConfigureResponse,
- connector: {
- id: 'resilient-2',
- name: 'My Resilient connector',
- type: ConnectorTypes.resilient,
- fields: null,
+ data: {
+ ...useCaseConfigureResponse.data,
+ connector: {
+ id: 'resilient-2',
+ name: 'My Resilient connector',
+ type: ConnectorTypes.resilient,
+ fields: null,
+ },
},
}));
@@ -469,26 +480,24 @@ describe('ConfigureCases', () => {
beforeEach(() => {
persistCaseConfigure = jest.fn();
- useCaseConfigureMock.mockImplementation(() => ({
+ useGetCaseConfigurationMock.mockImplementation(() => ({
...useCaseConfigureResponse,
- mapping: null,
- closureType: 'close-by-user',
- connector: {
- id: 'servicenow-1',
- name: 'My connector',
- type: ConnectorTypes.serviceNowITSM,
- fields: null,
- },
- currentConfiguration: {
+ data: {
+ ...useCaseConfigureResponse.data,
+ mapping: null,
+ closureType: 'close-by-user',
connector: {
- id: 'My connector',
+ id: 'servicenow-1',
name: 'My connector',
- type: ConnectorTypes.jira,
+ type: ConnectorTypes.serviceNowITSM,
fields: null,
},
- closureType: 'close-by-user',
},
- persistCaseConfigure,
+ }));
+
+ usePersistConfigurationMock.mockImplementation(() => ({
+ ...usePersistConfigurationMockResponse,
+ mutate: persistCaseConfigure,
}));
useGetConnectorsMock.mockImplementation(() => useConnectorsResponse);
useGetUrlSearchMock.mockImplementation(() => searchURL);
@@ -511,32 +520,31 @@ describe('ConfigureCases', () => {
fields: null,
},
closureType: 'close-by-pushing',
+ customFields: [],
+ id: '',
+ version: '',
});
});
});
describe('user interactions', () => {
beforeEach(() => {
- useCaseConfigureMock.mockImplementation(() => ({
+ useGetCaseConfigurationMock.mockImplementation(() => ({
...useCaseConfigureResponse,
- mapping: null,
- closureType: 'close-by-user',
- connector: {
- id: 'resilient-2',
- name: 'unchanged',
- type: ConnectorTypes.resilient,
- fields: null,
- },
- currentConfiguration: {
+ data: {
+ ...useCaseConfigureResponse.data,
+
+ mapping: null,
+ closureType: 'close-by-user',
connector: {
id: 'resilient-2',
name: 'unchanged',
- type: ConnectorTypes.serviceNowITSM,
+ type: ConnectorTypes.resilient,
fields: null,
},
- closureType: 'close-by-user',
},
}));
+
useGetConnectorsMock.mockImplementation(() => useConnectorsResponse);
useGetUrlSearchMock.mockImplementation(() => searchURL);
});
@@ -597,4 +605,267 @@ describe('ConfigureCases', () => {
).toBeFalsy();
});
});
+
+ describe('custom fields', () => {
+ let appMockRender: AppMockRenderer;
+ let persistCaseConfigure: jest.Mock;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ appMockRender = createAppMockRenderer();
+ persistCaseConfigure = jest.fn();
+ usePersistConfigurationMock.mockImplementation(() => ({
+ ...usePersistConfigurationMockResponse,
+ mutate: persistCaseConfigure,
+ }));
+ });
+
+ it('renders custom field group when no custom fields available', () => {
+ appMockRender.render();
+
+ expect(screen.getByTestId('custom-fields-form-group')).toBeInTheDocument();
+ });
+
+ it('renders custom field when available', () => {
+ const customFieldsMock: CustomFieldsConfiguration = [
+ {
+ key: 'random_custom_key',
+ label: 'summary',
+ type: CustomFieldTypes.TEXT,
+ required: true,
+ },
+ ];
+
+ useGetCaseConfigurationMock.mockImplementation(() => ({
+ ...useCaseConfigureResponse,
+ data: {
+ ...useCaseConfigureResponse.data,
+ customFields: customFieldsMock,
+ },
+ }));
+
+ appMockRender.render();
+
+ expect(
+ screen.getByTestId(`custom-field-${customFieldsMock[0].label}-${customFieldsMock[0].type}`)
+ ).toBeInTheDocument();
+ });
+
+ it('renders multiple custom field when available', () => {
+ useGetCaseConfigurationMock.mockImplementation(() => ({
+ ...useCaseConfigureResponse,
+ data: {
+ ...useCaseConfigureResponse.data,
+ customFields: customFieldsConfigurationMock,
+ },
+ }));
+
+ appMockRender.render();
+
+ const list = screen.getByTestId('custom-fields-list');
+
+ for (const field of customFieldsConfigurationMock) {
+ expect(
+ within(list).getByTestId(`custom-field-${field.label}-${field.type}`)
+ ).toBeInTheDocument();
+ }
+ });
+
+ it('deletes a custom field correctly', async () => {
+ useGetCaseConfigurationMock.mockImplementation(() => ({
+ ...useCaseConfigureResponse,
+ data: {
+ ...useCaseConfigureResponse.data,
+ customFields: customFieldsConfigurationMock,
+ },
+ }));
+
+ appMockRender.render();
+
+ const list = screen.getByTestId('custom-fields-list');
+
+ userEvent.click(
+ within(list).getByTestId(`${customFieldsConfigurationMock[0].key}-custom-field-delete`)
+ );
+
+ expect(await screen.findByTestId('confirm-delete-custom-field-modal')).toBeInTheDocument();
+
+ userEvent.click(screen.getByText('Delete'));
+
+ await waitFor(() => {
+ expect(persistCaseConfigure).toHaveBeenCalledWith({
+ connector: {
+ id: 'none',
+ name: 'none',
+ type: ConnectorTypes.none,
+ fields: null,
+ },
+ closureType: 'close-by-user',
+ customFields: [{ ...customFieldsConfigurationMock[1] }],
+ id: '',
+ version: '',
+ });
+ });
+ });
+
+ it('updates a custom field correctly', async () => {
+ useGetCaseConfigurationMock.mockImplementation(() => ({
+ ...useCaseConfigureResponse,
+ data: {
+ ...useCaseConfigureResponse.data,
+ customFields: customFieldsConfigurationMock,
+ },
+ }));
+
+ appMockRender.render();
+
+ const list = screen.getByTestId('custom-fields-list');
+
+ userEvent.click(
+ within(list).getByTestId(`${customFieldsConfigurationMock[0].key}-custom-field-edit`)
+ );
+
+ expect(await screen.findByTestId('custom-field-flyout')).toBeInTheDocument();
+
+ userEvent.paste(screen.getByTestId('custom-field-label-input'), '!!');
+
+ userEvent.click(screen.getByTestId('text-custom-field-options'));
+
+ userEvent.click(screen.getByTestId('custom-field-flyout-save'));
+
+ await waitFor(() => {
+ expect(persistCaseConfigure).toHaveBeenCalledWith({
+ connector: {
+ id: 'none',
+ name: 'none',
+ type: ConnectorTypes.none,
+ fields: null,
+ },
+ closureType: 'close-by-user',
+ customFields: [
+ {
+ ...customFieldsConfigurationMock[0],
+ label: `${customFieldsConfigurationMock[0].label}!!`,
+ required: !customFieldsConfigurationMock[0].required,
+ },
+ { ...customFieldsConfigurationMock[1] },
+ ],
+ id: '',
+ version: '',
+ });
+ });
+ });
+
+ it('opens fly out for when click on add field', async () => {
+ appMockRender.render();
+
+ userEvent.click(screen.getByTestId('add-custom-field'));
+
+ expect(await screen.findByTestId('custom-field-flyout')).toBeInTheDocument();
+ });
+
+ it('closes fly out for when click on cancel', async () => {
+ appMockRender.render();
+
+ userEvent.click(screen.getByTestId('add-custom-field'));
+
+ expect(await screen.findByTestId('custom-field-flyout')).toBeInTheDocument();
+
+ userEvent.click(screen.getByTestId('custom-field-flyout-cancel'));
+
+ expect(await screen.findByTestId('custom-fields-form-group')).toBeInTheDocument();
+ expect(screen.queryByTestId('custom-field-flyout')).not.toBeInTheDocument();
+ });
+
+ it('closes fly out for when click on save field', async () => {
+ appMockRender.render();
+
+ userEvent.click(screen.getByTestId('add-custom-field'));
+
+ expect(await screen.findByTestId('custom-field-flyout')).toBeInTheDocument();
+
+ userEvent.paste(screen.getByTestId('custom-field-label-input'), 'Summary');
+
+ userEvent.click(screen.getByTestId('custom-field-flyout-save'));
+
+ await waitFor(() => {
+ expect(persistCaseConfigure).toHaveBeenCalledWith({
+ connector: {
+ id: 'none',
+ name: 'none',
+ type: ConnectorTypes.none,
+ fields: null,
+ },
+ closureType: 'close-by-user',
+ customFields: [
+ ...customFieldsConfigurationMock,
+ {
+ key: expect.anything(),
+ label: 'Summary',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ ],
+ id: '',
+ version: '',
+ });
+ });
+
+ expect(screen.getByTestId('custom-fields-form-group')).toBeInTheDocument();
+ expect(screen.queryByTestId('custom-field-flyout')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('rendering with license limitations', () => {
+ let appMockRender: AppMockRenderer;
+ let persistCaseConfigure: jest.Mock;
+
+ beforeEach(() => {
+ // Default setup
+ jest.clearAllMocks();
+ useGetConnectorsMock.mockImplementation(() => ({ useConnectorsResponse }));
+ appMockRender = createAppMockRenderer();
+ persistCaseConfigure = jest.fn();
+ usePersistConfigurationMock.mockImplementation(() => ({
+ ...usePersistConfigurationMockResponse,
+ mutate: persistCaseConfigure,
+ }));
+ useGetCaseConfigurationMock.mockImplementation(() => useCaseConfigureResponse);
+
+ // Updated
+ useLicenseMock.mockReturnValue({ isAtLeastGold: () => false });
+ });
+
+ it('should not render connectors and closure options', () => {
+ appMockRender.render();
+ expect(screen.queryByTestId('dropdown-connectors')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('closure-options-radio-group')).not.toBeInTheDocument();
+ });
+
+ it('should render custom field section', () => {
+ appMockRender.render();
+ expect(screen.getByTestId('custom-fields-form-group')).toBeInTheDocument();
+ });
+
+ describe('when the previously selected connector doesnt appear due to license downgrade or because it was deleted', () => {
+ beforeEach(() => {
+ useGetCaseConfigurationMock.mockImplementation(() => ({
+ data: {
+ ...useCaseConfigureResponse.data,
+ closureType: 'close-by-user',
+ connector: {
+ id: 'not-id',
+ name: 'unchanged',
+ type: ConnectorTypes.none,
+ fields: null,
+ },
+ },
+ }));
+ });
+
+ it('should not render the warning callout', () => {
+ expect(screen.queryByTestId('configure-cases-warning-callout')).not.toBeInTheDocument();
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx
index fcbc455345b21..42a93622f1a52 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx
@@ -9,13 +9,14 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled, { css } from 'styled-components';
import { FormattedMessage } from '@kbn/i18n-react';
-import { EuiCallOut, EuiLink, EuiPageBody } from '@elastic/eui';
+import { EuiCallOut, EuiFlexItem, EuiLink, EuiPageBody } from '@elastic/eui';
import type { ActionConnectorTableItem } from '@kbn/triggers-actions-ui-plugin/public/types';
import { CasesConnectorFeatureId } from '@kbn/actions-plugin/common';
+import type { CustomFieldConfiguration } from '../../../common/types/domain';
import { useKibana } from '../../common/lib/kibana';
import { useGetActionTypes } from '../../containers/configure/use_action_types';
-import { useCaseConfigure } from '../../containers/configure/use_configure';
+import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration';
import type { ClosureType } from '../../containers/configure/types';
@@ -29,7 +30,12 @@ import { HeaderPage } from '../header_page';
import { useCasesContext } from '../cases_context/use_cases_context';
import { useCasesBreadcrumbs } from '../use_breadcrumbs';
import { CasesDeepLinkId } from '../../common/navigation';
+import { CustomFields } from '../custom_fields';
+import { CustomFieldFlyout } from '../custom_fields/flyout';
import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors';
+import { usePersistConfiguration } from '../../containers/configure/use_persist_configuration';
+import { addOrReplaceCustomField } from '../custom_fields/utils';
+import { useLicense } from '../../common/use_license';
const FormWrapper = styled.div`
${({ theme }) => css`
@@ -53,6 +59,8 @@ export const ConfigureCases: React.FC = React.memo(() => {
const { permissions } = useCasesContext();
const { triggersActionsUi } = useKibana().services;
useCasesBreadcrumbs(CasesDeepLinkId.casesConfigure);
+ const license = useLicense();
+ const hasMinimumLicensePermissions = license.isAtLeastGold();
const [connectorIsValid, setConnectorIsValid] = useState(true);
const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false);
@@ -60,18 +68,29 @@ export const ConfigureCases: React.FC = React.memo(() => {
const [editedConnectorItem, setEditedConnectorItem] = useState(
null
);
+ const [customFieldFlyoutVisible, setCustomFieldFlyoutVisibility] = useState(false);
+ const [customFieldToEdit, setCustomFieldToEdit] = useState(null);
const {
- connector,
- closureType,
- loading: loadingCaseConfigure,
- mappings,
- persistLoading,
- persistCaseConfigure,
- refetchCaseConfigure,
- setConnector,
- setClosureType,
- } = useCaseConfigure();
+ data: {
+ id: configurationId,
+ version: configurationVersion,
+ closureType,
+ connector,
+ mappings,
+ customFields,
+ },
+ isLoading: loadingCaseConfigure,
+ refetch: refetchCaseConfigure,
+ } = useGetCaseConfiguration();
+
+ const {
+ mutate: persistCaseConfigure,
+ mutateAsync: persistCaseConfigureAsync,
+ isLoading: isPersistingConfiguration,
+ } = usePersistConfiguration();
+
+ const isLoadingCaseConfiguration = loadingCaseConfigure || isPersistingConfiguration;
const {
isLoading: isLoadingConnectors,
@@ -98,18 +117,31 @@ export const ConfigureCases: React.FC = React.memo(() => {
async (createdConnector) => {
const caseConnector = normalizeActionConnector(createdConnector);
- await persistCaseConfigure({
+ await persistCaseConfigureAsync({
connector: caseConnector,
closureType,
+ customFields,
+ id: configurationId,
+ version: configurationVersion,
});
+
onConnectorUpdated(createdConnector);
- setConnector(caseConnector);
},
- [onConnectorUpdated, closureType, setConnector, persistCaseConfigure]
+ [
+ persistCaseConfigureAsync,
+ closureType,
+ customFields,
+ configurationId,
+ configurationVersion,
+ onConnectorUpdated,
+ ]
);
const isLoadingAny =
- isLoadingConnectors || persistLoading || loadingCaseConfigure || isLoadingActionTypes;
+ isLoadingConnectors ||
+ isPersistingConfiguration ||
+ loadingCaseConfigure ||
+ isLoadingActionTypes;
const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connector.id === 'none';
const onClickUpdateConnector = useCallback(() => {
setEditFlyoutVisibility(true);
@@ -133,24 +165,35 @@ export const ConfigureCases: React.FC = React.memo(() => {
const caseConnector =
actionConnector != null ? normalizeActionConnector(actionConnector) : getNoneConnector();
- setConnector(caseConnector);
persistCaseConfigure({
connector: caseConnector,
closureType,
+ customFields,
+ id: configurationId,
+ version: configurationVersion,
});
},
- [connectors, closureType, persistCaseConfigure, setConnector]
+ [
+ connectors,
+ persistCaseConfigure,
+ closureType,
+ customFields,
+ configurationId,
+ configurationVersion,
+ ]
);
const onChangeClosureType = useCallback(
(type: ClosureType) => {
- setClosureType(type);
persistCaseConfigure({
connector,
+ customFields,
+ id: configurationId,
+ version: configurationVersion,
closureType: type,
});
},
- [connector, persistCaseConfigure, setClosureType]
+ [configurationId, configurationVersion, connector, customFields, persistCaseConfigure]
);
useEffect(() => {
@@ -202,6 +245,88 @@ export const ConfigureCases: React.FC = React.memo(() => {
[connector.id, editedConnectorItem, editFlyoutVisible]
);
+ const onAddCustomFields = useCallback(() => {
+ setCustomFieldFlyoutVisibility(true);
+ }, [setCustomFieldFlyoutVisibility]);
+
+ const onDeleteCustomField = useCallback(
+ (key: string) => {
+ const remainingCustomFields = customFields.filter((field) => field.key !== key);
+
+ persistCaseConfigure({
+ connector,
+ customFields: [...remainingCustomFields],
+ id: configurationId,
+ version: configurationVersion,
+ closureType,
+ });
+ },
+ [
+ closureType,
+ configurationId,
+ configurationVersion,
+ connector,
+ customFields,
+ persistCaseConfigure,
+ ]
+ );
+
+ const onEditCustomField = useCallback(
+ (key: string) => {
+ const selectedCustomField = customFields.find((item) => item.key === key);
+
+ if (selectedCustomField) {
+ setCustomFieldToEdit(selectedCustomField);
+ }
+ setCustomFieldFlyoutVisibility(true);
+ },
+ [setCustomFieldFlyoutVisibility, setCustomFieldToEdit, customFields]
+ );
+
+ const onCloseAddFieldFlyout = useCallback(() => {
+ setCustomFieldFlyoutVisibility(false);
+ setCustomFieldToEdit(null);
+ }, [setCustomFieldFlyoutVisibility, setCustomFieldToEdit]);
+
+ const onSaveCustomField = useCallback(
+ (customFieldData: CustomFieldConfiguration) => {
+ const updatedFields = addOrReplaceCustomField(customFields, customFieldData);
+ persistCaseConfigure({
+ connector,
+ customFields: updatedFields,
+ id: configurationId,
+ version: configurationVersion,
+ closureType,
+ });
+
+ setCustomFieldFlyoutVisibility(false);
+ setCustomFieldToEdit(null);
+ },
+ [
+ closureType,
+ configurationId,
+ configurationVersion,
+ connector,
+ customFields,
+ persistCaseConfigure,
+ ]
+ );
+
+ const CustomFieldAddFlyout = customFieldFlyoutVisible ? (
+
+ ) : null;
+
return (
<>
{
/>
- {!connectorIsValid && (
-
-
-
- {i18n.LINK_APPROPRIATE_LICENSE}
-
- ),
- }}
+ {hasMinimumLicensePermissions && (
+ <>
+ {!connectorIsValid && (
+
+
+
+ {i18n.LINK_APPROPRIATE_LICENSE}
+
+ ),
+ }}
+ />
+
+
+ )}
+
+
+
+
+
-
-
+
+ >
)}
-
-
-
-
+
+
+
{ConnectorAddFlyout}
{ConnectorEditFlyout}
+ {CustomFieldAddFlyout}
>
diff --git a/x-pack/plugins/cases/public/components/configure_cases/translations.ts b/x-pack/plugins/cases/public/components/configure_cases/translations.ts
index ada2690ab7b03..e10f6fcad2fb9 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/translations.ts
+++ b/x-pack/plugins/cases/public/components/configure_cases/translations.ts
@@ -150,7 +150,7 @@ export const DEPRECATED_TOOLTIP_CONTENT = i18n.translate(
);
export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate('xpack.cases.configureCases.headerTitle', {
- defaultMessage: 'Configure cases',
+ defaultMessage: 'Settings',
});
export const CASES_WEBHOOK_MAPPINGS = i18n.translate(
diff --git a/x-pack/plugins/cases/public/components/create/connector.test.tsx b/x-pack/plugins/cases/public/components/create/connector.test.tsx
index b9e40df65c4a7..7bc4bfbde6162 100644
--- a/x-pack/plugins/cases/public/components/create/connector.test.tsx
+++ b/x-pack/plugins/cases/public/components/create/connector.test.tsx
@@ -27,18 +27,18 @@ import {
createAppMockRenderer,
TestProviders,
} from '../../common/mock';
-import { useCaseConfigure } from '../../containers/configure/use_configure';
+import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration';
import { useCaseConfigureResponse } from '../configure_cases/__mock__';
jest.mock('../connectors/resilient/use_get_incident_types');
jest.mock('../connectors/resilient/use_get_severity');
jest.mock('../connectors/servicenow/use_get_choices');
-jest.mock('../../containers/configure/use_configure');
+jest.mock('../../containers/configure/use_get_case_configuration');
const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock;
const useGetSeverityMock = useGetSeverity as jest.Mock;
const useGetChoicesMock = useGetChoices as jest.Mock;
-const useCaseConfigureMock = useCaseConfigure as jest.Mock;
+const useGetCaseConfigurationMock = useGetCaseConfiguration as jest.Mock;
const useGetIncidentTypesResponse = {
isLoading: false,
@@ -85,7 +85,7 @@ describe('Connector', () => {
useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse);
useGetSeverityMock.mockReturnValue(useGetSeverityResponse);
useGetChoicesMock.mockReturnValue(useGetChoicesResponse);
- useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse);
+ useGetCaseConfigurationMock.mockImplementation(() => useCaseConfigureResponse);
});
it('it renders', async () => {
diff --git a/x-pack/plugins/cases/public/components/create/connector.tsx b/x-pack/plugins/cases/public/components/create/connector.tsx
index 7422e671fa4bb..39e04f7bc0be3 100644
--- a/x-pack/plugins/cases/public/components/create/connector.tsx
+++ b/x-pack/plugins/cases/public/components/create/connector.tsx
@@ -14,7 +14,7 @@ import type { ActionConnector } from '../../../common/types/domain';
import { ConnectorSelector } from '../connector_selector/form';
import { ConnectorFieldsForm } from '../connectors/fields_form';
import { schema } from './schema';
-import { useCaseConfigure } from '../../containers/configure/use_configure';
+import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration';
import { getConnectorById, getConnectorsFormValidators } from '../utils';
import { useApplicationCapabilities } from '../../common/lib/kibana';
import * as i18n from '../../common/translations';
@@ -29,7 +29,11 @@ interface Props {
const ConnectorComponent: React.FC = ({ connectors, isLoading, isLoadingConnectors }) => {
const [{ connectorId }] = useFormData({ watch: ['connectorId'] });
const connector = getConnectorById(connectorId, connectors) ?? null;
- const { connector: configurationConnector } = useCaseConfigure();
+
+ const {
+ data: { connector: configurationConnector },
+ } = useGetCaseConfiguration();
+
const { actions } = useApplicationCapabilities();
const { permissions } = useCasesContext();
const hasReadPermissions = permissions.connectors && actions.read;
diff --git a/x-pack/plugins/cases/public/components/create/custom_fields.test.tsx b/x-pack/plugins/cases/public/components/create/custom_fields.test.tsx
new file mode 100644
index 0000000000000..06f6c17922221
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/create/custom_fields.test.tsx
@@ -0,0 +1,110 @@
+/*
+ * 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 { screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import type { AppMockRenderer } from '../../common/mock';
+import { createAppMockRenderer } from '../../common/mock';
+import { FormTestComponent } from '../../common/test_utils';
+import { customFieldsConfigurationMock } from '../../containers/mock';
+import { CustomFields } from './custom_fields';
+import * as i18n from './translations';
+
+describe('CustomFields', () => {
+ let appMockRender: AppMockRenderer;
+ const onSubmit = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ appMockRender = createAppMockRenderer();
+ });
+
+ it('renders correctly', () => {
+ appMockRender.render(
+
+
+
+ );
+
+ expect(screen.getByText(i18n.ADDITIONAL_FIELDS)).toBeInTheDocument();
+ expect(screen.getByTestId('create-case-custom-fields')).toBeInTheDocument();
+
+ for (const item of customFieldsConfigurationMock) {
+ expect(
+ screen.getByTestId(`${item.key}-${item.type}-create-custom-field`)
+ ).toBeInTheDocument();
+ }
+ });
+
+ it('should not show the custom fields if the configuration is empty', async () => {
+ appMockRender.render(
+
+
+
+ );
+
+ expect(screen.queryByText(i18n.ADDITIONAL_FIELDS)).not.toBeInTheDocument();
+ expect(screen.queryAllByTestId('create-custom-field', { exact: false }).length).toEqual(0);
+ });
+
+ it('should sort the custom fields correctly', async () => {
+ const reversedConfiguration = [...customFieldsConfigurationMock].reverse();
+
+ appMockRender.render(
+
+
+
+ );
+
+ const customFieldsWrapper = await screen.findByTestId('create-case-custom-fields');
+
+ const customFields = customFieldsWrapper.querySelectorAll('.euiFormRow');
+
+ expect(customFields).toHaveLength(2);
+
+ expect(customFields[0]).toHaveTextContent('My test label 1');
+ expect(customFields[1]).toHaveTextContent('My test label 2');
+ });
+
+ it('should update the custom fields', async () => {
+ appMockRender = createAppMockRenderer();
+
+ appMockRender.render(
+
+
+
+ );
+
+ const textField = customFieldsConfigurationMock[0];
+ const toggleField = customFieldsConfigurationMock[1];
+
+ userEvent.type(
+ screen.getByTestId(`${textField.key}-${textField.type}-create-custom-field`),
+ 'hello'
+ );
+ userEvent.click(
+ screen.getByTestId(`${toggleField.key}-${toggleField.type}-create-custom-field`)
+ );
+
+ userEvent.click(screen.getByText('Submit'));
+
+ await waitFor(() => {
+ // data, isValid
+ expect(onSubmit).toHaveBeenCalledWith(
+ {
+ customFields: {
+ [textField.key]: 'hello',
+ [toggleField.key]: true,
+ },
+ },
+ true
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/create/custom_fields.tsx b/x-pack/plugins/cases/public/components/create/custom_fields.tsx
new file mode 100644
index 0000000000000..cfc80c125a7b9
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/create/custom_fields.tsx
@@ -0,0 +1,67 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo } from 'react';
+import { sortBy } from 'lodash';
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
+
+import type { CasesConfigurationUI } from '../../../common/ui';
+import { builderMap as customFieldsBuilderMap } from '../custom_fields/builder';
+import * as i18n from './translations';
+
+interface Props {
+ isLoading: boolean;
+ customFieldsConfiguration: CasesConfigurationUI['customFields'];
+}
+
+const CustomFieldsComponent: React.FC = ({ isLoading, customFieldsConfiguration }) => {
+ const sortedCustomFields = useMemo(
+ () => sortCustomFieldsByLabel(customFieldsConfiguration),
+ [customFieldsConfiguration]
+ );
+
+ const customFieldsComponents = sortedCustomFields.map(
+ (customField: CasesConfigurationUI['customFields'][number]) => {
+ const customFieldFactory = customFieldsBuilderMap[customField.type];
+ const customFieldType = customFieldFactory().build();
+
+ const CreateComponent = customFieldType.Create;
+
+ return (
+
+ );
+ }
+ );
+
+ if (!customFieldsConfiguration.length) {
+ return null;
+ }
+
+ return (
+
+
+ {i18n.ADDITIONAL_FIELDS}
+
+
+ {customFieldsComponents}
+
+ );
+};
+
+CustomFieldsComponent.displayName = 'CustomFields';
+
+export const CustomFields = React.memo(CustomFieldsComponent);
+
+const sortCustomFieldsByLabel = (configCustomFields: CasesConfigurationUI['customFields']) => {
+ return sortBy(configCustomFields, (configCustomField) => {
+ return configCustomField.label;
+ });
+};
diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx
index 20502756db502..e7bd2fc754f34 100644
--- a/x-pack/plugins/cases/public/components/create/form.test.tsx
+++ b/x-pack/plugins/cases/public/components/create/form.test.tsx
@@ -14,12 +14,12 @@ import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
import { NONE_CONNECTOR_ID } from '../../../common/constants';
import type { FormHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
-import { connectorsMock } from '../../containers/mock';
+import { connectorsMock, customFieldsConfigurationMock } from '../../containers/mock';
import type { FormProps } from './schema';
import { schema } from './schema';
import type { CreateCaseFormProps } from './form';
import { CreateCaseForm } from './form';
-import { useCaseConfigure } from '../../containers/configure/use_configure';
+import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration';
import { useCaseConfigureResponse } from '../configure_cases/__mock__';
import { TestProviders } from '../../common/mock';
import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors';
@@ -28,13 +28,13 @@ import { useAvailableCasesOwners } from '../app/use_available_owners';
jest.mock('../../containers/use_get_tags');
jest.mock('../../containers/configure/use_get_supported_action_connectors');
-jest.mock('../../containers/configure/use_configure');
+jest.mock('../../containers/configure/use_get_case_configuration');
jest.mock('../markdown_editor/plugins/lens/use_lens_draft_comment');
jest.mock('../app/use_available_owners');
const useGetTagsMock = useGetTags as jest.Mock;
const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock;
-const useCaseConfigureMock = useCaseConfigure as jest.Mock;
+const useGetCaseConfigurationMock = useGetCaseConfiguration as jest.Mock;
const useAvailableOwnersMock = useAvailableCasesOwners as jest.Mock;
const initialCaseValue: FormProps = {
@@ -45,6 +45,7 @@ const initialCaseValue: FormProps = {
fields: null,
syncAlerts: true,
assignees: [],
+ customFields: {},
};
const casesFormProps: CreateCaseFormProps = {
@@ -81,7 +82,7 @@ describe('CreateCaseForm', () => {
useAvailableOwnersMock.mockReturnValue(['securitySolution', 'observability']);
useGetTagsMock.mockReturnValue({ data: ['test'] });
useGetConnectorsMock.mockReturnValue({ isLoading: false, data: connectorsMock });
- useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse);
+ useGetCaseConfigurationMock.mockImplementation(() => useCaseConfigureResponse);
});
afterEach(() => {
@@ -218,6 +219,30 @@ describe('CreateCaseForm', () => {
expect(descriptionInput).toHaveValue('');
});
+ it('should render custom fields when available', () => {
+ useGetCaseConfigurationMock.mockImplementation(() => ({
+ ...useCaseConfigureResponse,
+ data: {
+ ...useCaseConfigureResponse.data,
+ customFields: customFieldsConfigurationMock,
+ },
+ }));
+
+ const result = render(
+
+
+
+ );
+
+ expect(result.getByTestId('create-case-custom-fields')).toBeInTheDocument();
+
+ for (const item of customFieldsConfigurationMock) {
+ expect(
+ result.getByTestId(`${item.key}-${item.type}-create-custom-field`)
+ ).toBeInTheDocument();
+ }
+ });
+
it('should prefill the form when provided with initialValue', () => {
const { getByTestId } = render(
diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx
index 1c1668ac528fb..280adaa66aeec 100644
--- a/x-pack/plugins/cases/public/components/create/form.tsx
+++ b/x-pack/plugins/cases/public/components/create/form.tsx
@@ -19,6 +19,7 @@ import { useFormContext } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_
import type { ActionConnector } from '../../../common/types/domain';
import type { CasePostRequest } from '../../../common/types/api';
+import type { CasesConfigurationUI } from '../../../common/ui';
import { Title } from './title';
import { Description, fieldName as descriptionFieldName } from './description';
import { Tags } from './tags';
@@ -44,6 +45,7 @@ import { Assignees } from './assignees';
import { useCancelCreationAction } from './use_cancel_creation_action';
import { CancelCreationConfirmationModal } from './cancel_creation_confirmation_modal';
import { Category } from './category';
+import { CustomFields } from './custom_fields';
interface ContainerProps {
big?: boolean;
@@ -64,6 +66,8 @@ const MySpinner = styled(EuiLoadingSpinner)`
export interface CreateCaseFormFieldsProps {
connectors: ActionConnector[];
+ customFieldsConfiguration: CasesConfigurationUI['customFields'];
+ isLoadingCaseConfiguration: boolean;
isLoadingConnectors: boolean;
withSteps: boolean;
owner: string[];
@@ -83,7 +87,15 @@ export interface CreateCaseFormProps extends Pick = React.memo(
- ({ connectors, isLoadingConnectors, withSteps, owner, draftStorageKey }) => {
+ ({
+ connectors,
+ isLoadingConnectors,
+ withSteps,
+ owner,
+ draftStorageKey,
+ customFieldsConfiguration,
+ isLoadingCaseConfiguration,
+ }) => {
const { isSubmitting } = useFormContext();
const { isSyncAlertsEnabled, caseAssignmentAuthorized } = useCasesFeatures();
const availableOwners = useAvailableCasesOwners();
@@ -120,6 +132,12 @@ export const CreateCaseFormFields: React.FC = React.m
+
+
+
>
),
@@ -130,6 +148,8 @@ export const CreateCaseFormFields: React.FC = React.m
canShowCaseSolutionSelection,
availableOwners,
draftStorageKey,
+ customFieldsConfiguration,
+ isLoadingCaseConfiguration,
]
);
@@ -227,7 +247,9 @@ export const CreateCaseForm: React.FC = React.memo(
>
{
useCreateAttachmentsMock.mockImplementation(() => ({ mutateAsync: createAttachments }));
usePostPushToServiceMock.mockImplementation(() => defaultPostPushToService);
useGetConnectorsMock.mockReturnValue(sampleConnectorData);
- useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse);
+ useGetCaseConfigurationMock.mockImplementation(() => useCaseConfigureResponse);
useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse);
useGetSeverityMock.mockReturnValue(useGetSeverityResponse);
useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse);
@@ -429,16 +436,78 @@ describe.skip('Create case', () => {
await waitForComponentToUpdate();
});
+ it('should submit form with custom fields', async () => {
+ useGetCaseConfigurationMock.mockImplementation(() => ({
+ ...useCaseConfigureResponse,
+ data: {
+ ...useCaseConfigureResponse.data,
+ customFields: [
+ ...customFieldsConfigurationMock,
+ {
+ key: 'my_custom_field_key',
+ type: CustomFieldTypes.TEXT,
+ label: 'my custom field label',
+ required: false,
+ },
+ ],
+ },
+ }));
+
+ appMockRender.render(
+
+
+
+
+ );
+
+ await waitForFormToRender(screen);
+ await fillFormReactTestingLib({ renderer: screen });
+
+ const textField = customFieldsConfigurationMock[0];
+ const toggleField = customFieldsConfigurationMock[1];
+
+ expect(screen.getByTestId('create-case-custom-fields')).toBeInTheDocument();
+
+ userEvent.paste(
+ screen.getByTestId(`${textField.key}-${textField.type}-create-custom-field`),
+ 'My text test value 1'
+ );
+
+ userEvent.click(
+ screen.getByTestId(`${toggleField.key}-${toggleField.type}-create-custom-field`)
+ );
+
+ userEvent.click(screen.getByTestId('create-case-submit'));
+
+ await waitFor(() => expect(postCase).toHaveBeenCalled());
+
+ expect(postCase).toBeCalledWith({
+ request: {
+ ...sampleDataWithoutTags,
+ customFields: [
+ ...customFieldsMock,
+ {
+ key: 'my_custom_field_key',
+ type: CustomFieldTypes.TEXT,
+ value: null,
+ },
+ ],
+ },
+ });
+ });
+
it('should select the default connector set in the configuration', async () => {
- useCaseConfigureMock.mockImplementation(() => ({
+ useGetCaseConfigurationMock.mockImplementation(() => ({
...useCaseConfigureResponse,
- connector: {
- id: 'servicenow-1',
- name: 'SN',
- type: ConnectorTypes.serviceNowITSM,
- fields: null,
+ data: {
+ ...useCaseConfigureResponse.data,
+ connector: {
+ id: 'servicenow-1',
+ name: 'SN',
+ type: ConnectorTypes.serviceNowITSM,
+ fields: null,
+ },
},
- persistLoading: false,
}));
useGetConnectorsMock.mockReturnValue({
@@ -480,15 +549,17 @@ describe.skip('Create case', () => {
});
it('should default to none if the default connector does not exist in connectors', async () => {
- useCaseConfigureMock.mockImplementation(() => ({
+ useGetCaseConfigurationMock.mockImplementation(() => ({
...useCaseConfigureResponse,
- connector: {
- id: 'not-exist',
- name: 'SN',
- type: ConnectorTypes.serviceNowITSM,
- fields: null,
+ data: {
+ ...useCaseConfigureResponse.data,
+ connector: {
+ id: 'not-exist',
+ name: 'SN',
+ type: ConnectorTypes.serviceNowITSM,
+ fields: null,
+ },
},
- persistLoading: false,
}));
useGetConnectorsMock.mockReturnValue({
diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx
index 4df39cb2d52ac..2457b964ac3fc 100644
--- a/x-pack/plugins/cases/public/components/create/form_context.tsx
+++ b/x-pack/plugins/cases/public/components/create/form_context.tsx
@@ -15,7 +15,7 @@ import { getNoneConnector, normalizeActionConnector } from '../configure_cases/u
import { usePostCase } from '../../containers/use_post_case';
import { usePostPushToService } from '../../containers/use_post_push_to_service';
-import type { CaseUI } from '../../containers/types';
+import type { CaseUI, CaseUICustomField } from '../../containers/types';
import type { CasePostRequest } from '../../../common/types/api';
import type { UseCreateAttachments } from '../../containers/use_create_attachments';
import { useCreateAttachments } from '../../containers/use_create_attachments';
@@ -25,11 +25,13 @@ import {
getConnectorById,
getConnectorsFormDeserializer,
getConnectorsFormSerializer,
+ convertCustomFieldValue,
} from '../utils';
import { useAvailableCasesOwners } from '../app/use_available_owners';
import type { CaseAttachmentsWithoutOwner } from '../../types';
import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors';
import { useCreateCaseWithAttachmentsTransaction } from '../../common/apm/use_cases_transactions';
+import { useGetCaseConfiguration } from '../../containers/configure/use_get_case_configuration';
const initialCaseValue: FormProps = {
description: '',
@@ -41,6 +43,7 @@ const initialCaseValue: FormProps = {
syncAlerts: true,
selectedOwner: null,
assignees: [],
+ customFields: {},
};
interface Props {
@@ -63,6 +66,10 @@ export const FormContext: React.FC = ({
}) => {
const { data: connectors = [], isLoading: isLoadingConnectors } =
useGetSupportedActionConnectors();
+ const {
+ data: { customFields: customFieldsConfiguration },
+ isLoading: isLoadingCaseConfiguration,
+ } = useGetCaseConfiguration();
const { owner, appId } = useCasesContext();
const { isSyncAlertsEnabled } = useCasesFeatures();
const { mutateAsync: postCase } = usePostCase();
@@ -89,6 +96,30 @@ export const FormContext: React.FC = ({
return formData;
};
+ const transformCustomFieldsData = useCallback(
+ (customFields: Record) => {
+ const transformedCustomFields: CaseUI['customFields'] = [];
+
+ if (!customFields || !customFieldsConfiguration.length) {
+ return [];
+ }
+
+ for (const [key, value] of Object.entries(customFields)) {
+ const configCustomField = customFieldsConfiguration.find((item) => item.key === key);
+ if (configCustomField) {
+ transformedCustomFields.push({
+ key: configCustomField.key,
+ type: configCustomField.type,
+ value: convertCustomFieldValue(value),
+ } as CaseUICustomField);
+ }
+ }
+
+ return transformedCustomFields;
+ },
+ [customFieldsConfiguration]
+ );
+
const submitCase = useCallback(
async (
{
@@ -100,7 +131,7 @@ export const FormContext: React.FC = ({
isValid
) => {
if (isValid) {
- const { selectedOwner, ...userFormData } = dataWithoutConnectorId;
+ const { selectedOwner, customFields, ...userFormData } = dataWithoutConnectorId;
const caseConnector = getConnectorById(dataConnectorId, connectors);
const defaultOwner = owner[0] ?? availableOwners[0];
@@ -110,6 +141,8 @@ export const FormContext: React.FC = ({
? normalizeActionConnector(caseConnector, fields)
: getNoneConnector();
+ const transformedCustomFields = transformCustomFieldsData(customFields);
+
const trimmedData = trimUserFormData(userFormData);
const theCase = await postCase({
@@ -118,6 +151,7 @@ export const FormContext: React.FC = ({
connector: connectorToUpdate,
settings: { syncAlerts },
owner: selectedOwner ?? defaultOwner,
+ customFields: transformedCustomFields,
},
});
@@ -159,6 +193,7 @@ export const FormContext: React.FC = ({
onSuccess,
createAttachments,
pushCaseToExternalService,
+ transformCustomFieldsData,
]
);
@@ -175,10 +210,21 @@ export const FormContext: React.FC = ({
() =>
children != null
? React.Children.map(children, (child: React.ReactElement) =>
- React.cloneElement(child, { connectors, isLoadingConnectors })
+ React.cloneElement(child, {
+ connectors,
+ isLoadingConnectors,
+ customFieldsConfiguration,
+ isLoadingCaseConfiguration,
+ })
)
: null,
- [children, connectors, isLoadingConnectors]
+ [
+ children,
+ connectors,
+ isLoadingConnectors,
+ customFieldsConfiguration,
+ isLoadingCaseConfiguration,
+ ]
);
return (
+
+
+
+
+
+
+ }
+ betaBadgeProps={{
+ 'data-test-subj': 'obltOnboardingHomeQuickstartBadge',
+ color: 'accent',
+ label: i18n.translate(
+ 'xpack.observability_onboarding.card.systemLogs.quickstartBadge',
+ { defaultMessage: 'Quick start' }
+ ),
+ }}
+ title={i18n.translate(
+ 'xpack.observability_onboarding.card.systemLogs.title',
+ { defaultMessage: 'Stream host system logs' }
+ )}
+ footer={
+
+ {getStartedLabel}
+
}
- )}
- footer={
+ style={{
+ borderColor: euiTheme.colors.accent,
+ borderWidth: 2,
+ }}
+ paddingSize="m"
+ display="plain"
+ hasBorder
+ onClick={handleClickSystemLogs}
+ >
+
+ {elasticAgentLabel}
+
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.card.systemLogs.description1',
+ {
+ defaultMessage:
+ 'The quickest path to onboard log data from your own machine or server.',
+ }
+ )}
+
+
+
+
+
+
+ }
+ title={i18n.translate(
+ 'xpack.observability_onboarding.card.customLogs.title',
+ { defaultMessage: 'Stream log files' }
+ )}
+ footer={
+
+ {getStartedLabel}
+
+ }
+ paddingSize="m"
+ display="plain"
+ hasBorder
+ onClick={handleClickCustomLogs}
+ >
+
+ {elasticAgentLabel}
+
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.card.customLogs.description.text',
+ {
+ defaultMessage:
+ 'Stream any logs into Elastic in a simple way and explore their data.',
+ }
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ title={i18n.translate(
+ 'xpack.observability_onboarding.card.apm.title',
+ {
+ defaultMessage: 'Collect application performance data',
+ }
+ )}
+ description={
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.card.apm.description',
+ {
+ defaultMessage:
+ 'Collect traces, logs, and metrics from OpenTelemetry or APM custom agent.',
+ }
+ )}
+
+
+ }
+ footer={
+
+ {getStartedLabel}
+
+ }
+ paddingSize="m"
+ titleSize="xs"
+ display="plain"
+ hasBorder
+ onClick={handleClickApmSetupGuide}
+ />
+
+
+ }
+ title={i18n.translate(
+ 'xpack.observability_onboarding.card.k8s.title',
+ { defaultMessage: 'Collect Kubernetes clusters data' }
+ )}
+ description={
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.card.k8s.description',
+ {
+ defaultMessage:
+ 'Collect logs and metrics from Kubernetes clusters with Elastic agent.',
+ }
+ )}
+
+
+ }
+ footer={
+
+ {getStartedLabel}
+
+ }
+ titleSize="xs"
+ paddingSize="m"
+ display="plain"
+ hasBorder
+ onClick={handleClickKubernetesSetupGuide}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ title={i18n.translate(
+ 'xpack.observability_onboarding.card.integrations.title',
+ {
+ defaultMessage:
+ 'Explore 300+ ways of ingesting data with our integrations',
+ }
+ )}
+ footer={
+ <>
- {getStartedLabel}
-
- }
- titleSize="xs"
- paddingSize="m"
- display="plain"
- hasBorder
- onClick={handleClickKubernetesSetupGuide}
- />
-
-
-
-
-
-
-
-
-
-
-
-
- }
- title={i18n.translate(
- 'xpack.observability_onboarding.card.integrations.title',
- {
- defaultMessage:
- 'Explore 300+ ways of ingesting data with our integrations',
- }
- )}
- footer={
- <>
-
- {i18n.translate(
- 'xpack.observability_onboarding.card.integrations.start',
- { defaultMessage: 'Start exploring' }
- )}
-
-
-
-
{i18n.translate(
- 'xpack.observability_onboarding.card.integrations.quickLinks',
- { defaultMessage: 'Quick links:' }
+ 'xpack.observability_onboarding.card.integrations.start',
+ { defaultMessage: 'Start exploring' }
)}
-
-
-
-
+
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.card.integrations.quickLinks',
+ { defaultMessage: 'Quick links:' }
+ )}
+
+
+
- {i18n.translate(
- 'xpack.observability_onboarding.card.integrations.sampleData',
- { defaultMessage: 'Use sample data' }
- )}
-
-
-
- {i18n.translate(
- 'xpack.observability_onboarding.card.integrations.uploadFile',
- { defaultMessage: 'Upload a file' }
- )}
-
-
-
-
- {i18n.translate(
- 'xpack.observability_onboarding.card.integrations.awsFirehose',
- { defaultMessage: 'AWS Firehose' }
- )}
-
-
-
-
-
-
- >
- }
- titleSize="xs"
- paddingSize="none"
- display="plain"
- hasBorder
- />
-
-
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.card.integrations.sampleData',
+ { defaultMessage: 'Use sample data' }
+ )}
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.card.integrations.uploadFile',
+ { defaultMessage: 'Upload a file' }
+ )}
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.observability_onboarding.card.integrations.awsFirehose',
+ { defaultMessage: 'AWS Firehose' }
+ )}
+
+
+
+
+
+
+
+ >
+ }
+ titleSize="xs"
+ paddingSize="m"
+ display="plain"
+ hasBorder
+ />
+
+
+
);
}
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.gen.ts
new file mode 100644
index 0000000000000..4ca5d3a41b6fa
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.gen.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { z } from 'zod';
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ */
+
+import { RuleSignatureId } from './rule_schema/common_attributes.gen';
+
+export type ErrorSchema = z.infer;
+export const ErrorSchema = z
+ .object({
+ id: z.string().optional(),
+ rule_id: RuleSignatureId.optional(),
+ list_id: z.string().min(1).optional(),
+ item_id: z.string().min(1).optional(),
+ error: z.object({
+ status_code: z.number().int().min(400),
+ message: z.string(),
+ }),
+ })
+ .strict();
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.schema.yaml
index 7e9a11ccf56dc..1bcffdd010bd9 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.schema.yaml
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.schema.yaml
@@ -4,12 +4,13 @@ info:
version: 'not applicable'
paths: {}
components:
- x-codegen-enabled: false
+ x-codegen-enabled: true
schemas:
ErrorSchema:
type: object
required:
- error
+ additionalProperties: false
properties:
id:
type: string
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.test.ts
index 4d5bf2c3f2947..8326479db9c14 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.test.ts
@@ -8,14 +8,13 @@
import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
import { left } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
-import type { ErrorSchema } from './error_schema';
-import { errorSchema } from './error_schema';
+import { ErrorSchema } from './error_schema';
import { getErrorSchemaMock } from './error_schema.mock';
describe('error_schema', () => {
test('it should validate an error with a UUID given for id', () => {
const error = getErrorSchemaMock();
- const decoded = errorSchema.decode(getErrorSchemaMock());
+ const decoded = ErrorSchema.decode(getErrorSchemaMock());
const checked = exactCheck(error, decoded);
const message = pipe(checked, foldLeftRight);
@@ -25,7 +24,7 @@ describe('error_schema', () => {
test('it should validate an error with a plain string given for id since sometimes we echo the user id which might not be a UUID back out to them', () => {
const error = getErrorSchemaMock('fake id');
- const decoded = errorSchema.decode(error);
+ const decoded = ErrorSchema.decode(error);
const checked = exactCheck(error, decoded);
const message = pipe(checked, foldLeftRight);
@@ -37,7 +36,7 @@ describe('error_schema', () => {
type InvalidError = ErrorSchema & { invalid_extra_data?: string };
const error: InvalidError = getErrorSchemaMock();
error.invalid_extra_data = 'invalid_extra_data';
- const decoded = errorSchema.decode(error);
+ const decoded = ErrorSchema.decode(error);
const checked = exactCheck(error, decoded);
const message = pipe(checked, foldLeftRight);
@@ -49,7 +48,7 @@ describe('error_schema', () => {
const error = getErrorSchemaMock();
// @ts-expect-error
delete error.error;
- const decoded = errorSchema.decode(error);
+ const decoded = ErrorSchema.decode(error);
const checked = exactCheck(error, decoded);
const message = pipe(checked, foldLeftRight);
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.ts
index e6f2fecbf7e16..53114d500bc21 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.ts
@@ -31,5 +31,5 @@ const required = t.exact(
})
);
-export const errorSchema = t.intersection([partial, required]);
-export type ErrorSchema = t.TypeOf;
+export const ErrorSchema = t.intersection([partial, required]);
+export type ErrorSchema = t.TypeOf;
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts
new file mode 100644
index 0000000000000..5ce6fe1bc4727
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts
@@ -0,0 +1,207 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { z } from 'zod';
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ */
+
+/**
+ * A universally unique identifier
+ */
+export type UUID = z.infer;
+export const UUID = z.string();
+
+export type RuleObjectId = z.infer;
+export const RuleObjectId = z.string();
+
+/**
+ * Could be any string, not necessarily a UUID
+ */
+export type RuleSignatureId = z.infer;
+export const RuleSignatureId = z.string();
+
+export type RuleName = z.infer;
+export const RuleName = z.string().min(1);
+
+export type RuleDescription = z.infer;
+export const RuleDescription = z.string().min(1);
+
+export type RuleVersion = z.infer;
+export const RuleVersion = z.string();
+
+export type IsRuleImmutable = z.infer;
+export const IsRuleImmutable = z.boolean();
+
+export type IsRuleEnabled = z.infer;
+export const IsRuleEnabled = z.boolean();
+
+export type RuleTagArray = z.infer;
+export const RuleTagArray = z.array(z.string());
+
+export type RuleMetadata = z.infer;
+export const RuleMetadata = z.object({});
+
+export type RuleLicense = z.infer;
+export const RuleLicense = z.string();
+
+export type RuleAuthorArray = z.infer;
+export const RuleAuthorArray = z.array(z.string());
+
+export type RuleFalsePositiveArray = z.infer;
+export const RuleFalsePositiveArray = z.array(z.string());
+
+export type RuleReferenceArray = z.infer;
+export const RuleReferenceArray = z.array(z.string());
+
+export type InvestigationGuide = z.infer;
+export const InvestigationGuide = z.string();
+
+export type SetupGuide = z.infer;
+export const SetupGuide = z.string();
+
+export type BuildingBlockType = z.infer;
+export const BuildingBlockType = z.string();
+
+export type AlertsIndex = z.infer;
+export const AlertsIndex = z.string();
+
+export type AlertsIndexNamespace = z.infer;
+export const AlertsIndexNamespace = z.string();
+
+export type MaxSignals = z.infer;
+export const MaxSignals = z.number().int().min(1);
+
+export type Subtechnique = z.infer;
+export const Subtechnique = z.object({
+ /**
+ * Subtechnique ID
+ */
+ id: z.string(),
+ /**
+ * Subtechnique name
+ */
+ name: z.string(),
+ /**
+ * Subtechnique reference
+ */
+ reference: z.string(),
+});
+
+export type Technique = z.infer;
+export const Technique = z.object({
+ /**
+ * Technique ID
+ */
+ id: z.string(),
+ /**
+ * Technique name
+ */
+ name: z.string(),
+ /**
+ * Technique reference
+ */
+ reference: z.string(),
+ /**
+ * Array containing more specific information on the attack technique
+ */
+ subtechnique: z.array(Subtechnique).optional(),
+});
+
+export type Threat = z.infer;
+export const Threat = z.object({
+ /**
+ * Relevant attack framework
+ */
+ framework: z.string(),
+ tactic: z.object({
+ /**
+ * Tactic ID
+ */
+ id: z.string(),
+ /**
+ * Tactic name
+ */
+ name: z.string(),
+ /**
+ * Tactic reference
+ */
+ reference: z.string(),
+ }),
+ /**
+ * Array containing information on the attack techniques (optional)
+ */
+ technique: z.array(Technique).optional(),
+});
+
+export type ThreatArray = z.infer;
+export const ThreatArray = z.array(Threat);
+
+export type IndexPatternArray = z.infer;
+export const IndexPatternArray = z.array(z.string());
+
+export type DataViewId = z.infer;
+export const DataViewId = z.string();
+
+export type RuleQuery = z.infer;
+export const RuleQuery = z.string();
+
+export type RuleFilterArray = z.infer;
+export const RuleFilterArray = z.array(z.object({}));
+
+export type RuleNameOverride = z.infer;
+export const RuleNameOverride = z.string();
+
+export type TimestampOverride = z.infer;
+export const TimestampOverride = z.string();
+
+export type TimestampOverrideFallbackDisabled = z.infer;
+export const TimestampOverrideFallbackDisabled = z.boolean();
+
+export type RequiredField = z.infer;
+export const RequiredField = z.object({
+ name: z.string().min(1).optional(),
+ type: z.string().min(1).optional(),
+ ecs: z.boolean().optional(),
+});
+
+export type RequiredFieldArray = z.infer;
+export const RequiredFieldArray = z.array(RequiredField);
+
+export type TimelineTemplateId = z.infer;
+export const TimelineTemplateId = z.string();
+
+export type TimelineTemplateTitle = z.infer;
+export const TimelineTemplateTitle = z.string();
+
+export type SavedObjectResolveOutcome = z.infer;
+export const SavedObjectResolveOutcome = z.enum(['exactMatch', 'aliasMatch', 'conflict']);
+export const SavedObjectResolveOutcomeEnum = SavedObjectResolveOutcome.enum;
+export type SavedObjectResolveOutcomeEnum = typeof SavedObjectResolveOutcome.enum;
+
+export type SavedObjectResolveAliasTargetId = z.infer;
+export const SavedObjectResolveAliasTargetId = z.string();
+
+export type SavedObjectResolveAliasPurpose = z.infer;
+export const SavedObjectResolveAliasPurpose = z.enum([
+ 'savedObjectConversion',
+ 'savedObjectImport',
+]);
+export const SavedObjectResolveAliasPurposeEnum = SavedObjectResolveAliasPurpose.enum;
+export type SavedObjectResolveAliasPurposeEnum = typeof SavedObjectResolveAliasPurpose.enum;
+
+export type RelatedIntegration = z.infer;
+export const RelatedIntegration = z.object({
+ package: z.string().min(1),
+ version: z.string().min(1),
+ integration: z.string().min(1).optional(),
+});
+
+export type RelatedIntegrationArray = z.infer;
+export const RelatedIntegrationArray = z.array(RelatedIntegration);
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml
index 0e5a602e71018..52d59c3a656d6 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml
@@ -4,7 +4,7 @@ info:
version: 'not applicable'
paths: {}
components:
- x-codegen-enabled: false
+ x-codegen-enabled: true
schemas:
UUID:
type: string
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts
new file mode 100644
index 0000000000000..4615a7b0b466f
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.gen.ts
@@ -0,0 +1,547 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { z } from 'zod';
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ */
+
+export type Action = z.infer;
+export const Action = z.object({
+ /**
+ * The action type used for sending notifications.
+ */
+ action_type_id: z.string(),
+ /**
+ * Optionally groups actions by use cases. Use `default` for alert notifications.
+ */
+ group: z.string(),
+ /**
+ * The connector ID.
+ */
+ id: z.string(),
+ /**
+ * Object containing the allowed connector fields, which varies according to the connector type.
+ */
+ params: z.object({}),
+ uuid: z.string().optional(),
+ /**
+ * TODO implement the schema type
+ */
+ alerts_filter: z.object({}).optional(),
+ /**
+ * TODO implement the schema type
+ */
+ frequency: z.object({}).optional(),
+});
+
+export type AlertSuppression = z.infer;
+export const AlertSuppression = z.object({
+ group_by: z.array(z.string()).min(1).max(3),
+ duration: z
+ .object({
+ value: z.number().int().min(1),
+ unit: z.enum(['s', 'm', 'h']),
+ })
+ .optional(),
+ missing_fields_strategy: z.enum(['doNotSuppress', 'suppress']).optional(),
+});
+
+export type BaseRule = z.infer;
+export const BaseRule = z.object({
+ /**
+ * Rule name
+ */
+ name: z.string(),
+ /**
+ * Rule description
+ */
+ description: z.string(),
+ /**
+ * Risk score (0 to 100)
+ */
+ risk_score: z.number().int().min(0).max(100),
+ /**
+ * Severity of the rule
+ */
+ severity: z.enum(['low', 'medium', 'high', 'critical']),
+ /**
+ * Sets the source field for the alert's signal.rule.name value
+ */
+ rule_name_override: z.string().optional(),
+ /**
+ * Sets the time field used to query indices (optional)
+ */
+ timestamp_override: z.string().optional(),
+ /**
+ * Timeline template ID
+ */
+ timeline_id: z.string().optional(),
+ /**
+ * Timeline template title
+ */
+ timeline_title: z.string().optional(),
+ outcome: z.enum(['exactMatch', 'aliasMatch', 'conflict']).optional(),
+ /**
+ * TODO
+ */
+ alias_target_id: z.string().optional(),
+ /**
+ * TODO
+ */
+ alias_purpose: z.enum(['savedObjectConversion', 'savedObjectImport']).optional(),
+ /**
+ * The rule’s license.
+ */
+ license: z.string().optional(),
+ /**
+ * Notes to help investigate alerts produced by the rule.
+ */
+ note: z.string().optional(),
+ /**
+ * Determines if the rule acts as a building block. By default, building-block alerts are not displayed in the UI. These rules are used as a foundation for other rules that do generate alerts. Its value must be default.
+ */
+ building_block_type: z.string().optional(),
+ /**
+ * (deprecated) Has no effect.
+ */
+ output_index: z.string().optional(),
+ /**
+ * Has no effect.
+ */
+ namespace: z.string().optional(),
+ /**
+ * Stores rule metadata.
+ */
+ meta: z.object({}).optional(),
+ /**
+ * Defines the interval on which a rule's actions are executed.
+ */
+ throttle: z.string().optional(),
+ /**
+ * The rule’s version number. Defaults to 1.
+ */
+ version: z.number().int().min(1).optional().default(1),
+ /**
+ * String array containing words and phrases to help categorize, filter, and search rules. Defaults to an empty array.
+ */
+ tags: z.array(z.string()).optional().default([]),
+ /**
+ * Determines whether the rule is enabled. Defaults to true.
+ */
+ enabled: z.boolean().optional().default(true),
+ /**
+ * Overrides generated alerts' risk_score with a value from the source event
+ */
+ risk_score_mapping: z
+ .array(
+ z.object({
+ field: z.string(),
+ operator: z.enum(['equals']),
+ value: z.string(),
+ risk_score: z.number().int().min(0).max(100).optional(),
+ })
+ )
+ .optional()
+ .default([]),
+ /**
+ * Overrides generated alerts' severity with values from the source event
+ */
+ severity_mapping: z
+ .array(
+ z.object({
+ field: z.string(),
+ operator: z.enum(['equals']),
+ severity: z.enum(['low', 'medium', 'high', 'critical']),
+ value: z.string(),
+ })
+ )
+ .optional()
+ .default([]),
+ /**
+ * Frequency of rule execution, using a date math range. For example, "1h" means the rule runs every hour. Defaults to 5m (5 minutes).
+ */
+ interval: z.string().optional().default('5m'),
+ /**
+ * Time from which data is analyzed each time the rule executes, using a date math range. For example, now-4200s means the rule analyzes data from 70 minutes before its start time. Defaults to now-6m (analyzes data from 6 minutes before the start time).
+ */
+ from: z.string().optional().default('now-6m'),
+ /**
+ * TODO
+ */
+ to: z.string().optional().default('now'),
+ actions: z.array(Action).optional().default([]),
+ exceptions_list: z
+ .array(
+ z.object({
+ /**
+ * ID of the exception container
+ */
+ id: z.string().min(1),
+ /**
+ * List ID of the exception container
+ */
+ list_id: z.string().min(1),
+ /**
+ * The exception type
+ */
+ type: z.enum([
+ 'detection',
+ 'rule_default',
+ 'endpoint',
+ 'endpoint_trusted_apps',
+ 'endpoint_events',
+ 'endpoint_host_isolation_exceptions',
+ 'endpoint_blocklists',
+ ]),
+ /**
+ * Determines the exceptions validity in rule's Kibana space
+ */
+ namespace_type: z.enum(['agnostic', 'single']),
+ })
+ )
+ .optional()
+ .default([]),
+ author: z.array(z.string()).optional().default([]),
+ false_positives: z.array(z.string()).optional().default([]),
+ references: z.array(z.string()).optional().default([]),
+ max_signals: z.number().int().min(1).optional().default(100),
+ threat: z
+ .array(
+ z.object({
+ /**
+ * Relevant attack framework
+ */
+ framework: z.string(),
+ tactic: z.object({
+ id: z.string(),
+ name: z.string(),
+ reference: z.string(),
+ }),
+ technique: z
+ .array(
+ z.object({
+ id: z.string(),
+ name: z.string(),
+ reference: z.string(),
+ subtechnique: z
+ .array(
+ z.object({
+ id: z.string(),
+ name: z.string(),
+ reference: z.string(),
+ })
+ )
+ .optional(),
+ })
+ )
+ .optional(),
+ })
+ )
+ .optional(),
+});
+
+export type QueryRule = z.infer;
+export const QueryRule = BaseRule.and(
+ z.object({
+ /**
+ * Rule type
+ */
+ type: z.enum(['query']),
+ index: z.array(z.string()).optional(),
+ data_view_id: z.string().optional(),
+ filters: z.array(z.unknown()).optional(),
+ saved_id: z.string().optional(),
+ /**
+ * TODO
+ */
+ response_actions: z.array(z.object({})).optional(),
+ alert_suppression: AlertSuppression.optional(),
+ /**
+ * Query to execute
+ */
+ query: z.string().optional().default(''),
+ /**
+ * Query language to use.
+ */
+ language: z.enum(['kuery', 'lucene']).optional().default('kuery'),
+ })
+);
+
+export type SavedQueryRule = z.infer;
+export const SavedQueryRule = BaseRule.and(
+ z.object({
+ /**
+ * Rule type
+ */
+ type: z.enum(['saved_query']),
+ index: z.array(z.string()).optional(),
+ data_view_id: z.string().optional(),
+ filters: z.array(z.unknown()).optional(),
+ saved_id: z.string(),
+ /**
+ * TODO
+ */
+ response_actions: z.array(z.object({})).optional(),
+ alert_suppression: AlertSuppression.optional(),
+ /**
+ * Query to execute
+ */
+ query: z.string().optional(),
+ /**
+ * Query language to use.
+ */
+ language: z.enum(['kuery', 'lucene']).optional().default('kuery'),
+ })
+);
+
+export type ThresholdRule = z.infer;
+export const ThresholdRule = BaseRule.and(
+ z.object({
+ /**
+ * Rule type
+ */
+ type: z.enum(['threshold']),
+ query: z.string(),
+ threshold: z.object({
+ /**
+ * Field to aggregate on
+ */
+ field: z.union([z.string(), z.array(z.string())]),
+ /**
+ * Threshold value
+ */
+ value: z.number().int().min(1).optional(),
+ cardinality: z
+ .array(
+ z.object({
+ field: z.string().optional(),
+ value: z.number().int().min(0).optional(),
+ })
+ )
+ .optional(),
+ }),
+ index: z.array(z.string()).optional(),
+ data_view_id: z.string().optional(),
+ filters: z.array(z.unknown()).optional(),
+ saved_id: z.string().optional(),
+ /**
+ * Query language to use.
+ */
+ language: z.enum(['kuery', 'lucene']).optional().default('kuery'),
+ })
+);
+
+export type ThreatMatchRule = z.infer;
+export const ThreatMatchRule = BaseRule.and(
+ z.object({
+ /**
+ * Rule type
+ */
+ type: z.enum(['threat_match']),
+ query: z.string(),
+ /**
+ * Query to execute
+ */
+ threat_query: z.string(),
+ threat_mapping: z
+ .array(
+ z.object({
+ entries: z
+ .array(
+ z.object({
+ field: z.string().min(1).optional(),
+ type: z.enum(['mapping']).optional(),
+ value: z.string().min(1).optional(),
+ })
+ )
+ .optional(),
+ })
+ )
+ .min(1),
+ threat_index: z.array(z.string()),
+ index: z.array(z.string()).optional(),
+ data_view_id: z.string().optional(),
+ filters: z.array(z.unknown()).optional(),
+ saved_id: z.string().optional(),
+ threat_filters: z.array(z.unknown()).optional(),
+ /**
+ * Defines the path to the threat indicator in the indicator documents (optional)
+ */
+ threat_indicator_path: z.string().optional(),
+ /**
+ * Query language to use.
+ */
+ threat_language: z.enum(['kuery', 'lucene']).optional(),
+ concurrent_searches: z.number().int().min(1).optional(),
+ items_per_search: z.number().int().min(1).optional(),
+ /**
+ * Query language to use.
+ */
+ language: z.enum(['kuery', 'lucene']).optional().default('kuery'),
+ })
+);
+
+export type MlRule = z.infer;
+export const MlRule = BaseRule.and(
+ z.object({
+ /**
+ * Rule type
+ */
+ type: z.enum(['machine_learning']),
+ /**
+ * Anomaly threshold
+ */
+ anomaly_threshold: z.number().int().min(0),
+ /**
+ * Machine learning job ID
+ */
+ machine_learning_job_id: z.union([z.string(), z.array(z.string()).min(1)]),
+ })
+);
+
+export type EqlRule = z.infer;
+export const EqlRule = BaseRule.and(
+ z.object({
+ /**
+ * Rule type
+ */
+ type: z.enum(['eql']),
+ language: z.enum(['eql']),
+ /**
+ * EQL query to execute
+ */
+ query: z.string(),
+ index: z.array(z.string()).optional(),
+ data_view_id: z.string().optional(),
+ filters: z.array(z.unknown()).optional(),
+ /**
+ * Contains the event classification
+ */
+ event_category_field: z.string().optional(),
+ /**
+ * Sets a secondary field for sorting events
+ */
+ tiebreaker_field: z.string().optional(),
+ /**
+ * Contains the event timestamp used for sorting a sequence of events
+ */
+ timestamp_field: z.string().optional(),
+ })
+);
+
+export type NewTermsRule = z.infer;
+export const NewTermsRule = BaseRule.and(
+ z.object({
+ /**
+ * Rule type
+ */
+ type: z.enum(['new_terms']),
+ query: z.string(),
+ new_terms_fields: z.array(z.string()).min(1).max(3),
+ history_window_size: z.string().min(1).optional(),
+ index: z.array(z.string()).optional(),
+ data_view_id: z.string().optional(),
+ filters: z.array(z.unknown()).optional(),
+ language: z.enum(['kuery', 'lucene']).optional().default('kuery'),
+ })
+);
+
+export type Rule = z.infer;
+export const Rule = z.union([
+ QueryRule,
+ SavedQueryRule,
+ ThresholdRule,
+ ThreatMatchRule,
+ MlRule,
+ EqlRule,
+ NewTermsRule,
+]);
+
+/**
+ * Defines the maximum interval in which a rule's actions are executed.
+ */
+export type Throttle = z.infer;
+export const Throttle = z.enum(['rule', '1h', '1d', '7d']);
+export const ThrottleEnum = Throttle.enum;
+export type ThrottleEnum = typeof Throttle.enum;
+
+export type Subtechnique = z.infer;
+export const Subtechnique = z.object({
+ /**
+ * Subtechnique ID
+ */
+ id: z.string(),
+ /**
+ * Subtechnique name
+ */
+ name: z.string(),
+ /**
+ * Subtechnique reference
+ */
+ reference: z.string(),
+});
+
+export type Technique = z.infer;
+export const Technique = z.object({
+ /**
+ * Technique ID
+ */
+ id: z.string(),
+ /**
+ * Technique name
+ */
+ name: z.string(),
+ /**
+ * Technique reference
+ */
+ reference: z.string(),
+ /**
+ * Array containing more specific information on the attack technique
+ */
+ subtechnique: z.array(Subtechnique).optional(),
+});
+
+export type Threat = z.infer;
+export const Threat = z.object({
+ /**
+ * Relevant attack framework
+ */
+ framework: z.string(),
+ tactic: z.object({
+ /**
+ * Tactic ID
+ */
+ id: z.string(),
+ /**
+ * Tactic name
+ */
+ name: z.string(),
+ /**
+ * Tactic reference
+ */
+ reference: z.string(),
+ }),
+ /**
+ * Array containing information on the attack techniques (optional)
+ */
+ technique: z.array(Technique).optional(),
+});
+
+export type RuleResponse = z.infer;
+export const RuleResponse = z.object({});
+
+export type RuleCreateProps = z.infer;
+export const RuleCreateProps = z.object({});
+
+export type RuleUpdateProps = z.infer;
+export const RuleUpdateProps = z.object({});
+
+export type RulePatchProps = z.infer;
+export const RulePatchProps = z.object({});
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml
index a4cdcae498e7a..16cc2aec5cf2b 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml
@@ -4,98 +4,8 @@ info:
version: 'not applicable'
paths: {}
components:
- x-codegen-enabled: false
+ x-codegen-enabled: true
schemas:
- SortOrder:
- type: string
- enum:
- - asc
- - desc
- RuleExecutionStatus:
- type: string
- description: |-
- Custom execution status of Security rules that is different from the status used in the Alerting Framework. We merge our custom status with the Framework's status to determine the resulting status of a rule.
- - going to run - @deprecated Replaced by the 'running' status but left for backwards compatibility with rule execution events already written to Event Log in the prior versions of Kibana. Don't use when writing rule status changes.
- - running - Rule execution started but not reached any intermediate or final status.
- - partial failure - Rule can partially fail for various reasons either in the middle of an execution (in this case we update its status right away) or in the end of it. So currently this status can be both intermediate and final at the same time. A typical reason for a partial failure: not all the indices that the rule searches over actually exist.
- - failed - Rule failed to execute due to unhandled exception or a reason defined in the business logic of its executor function.
- - succeeded - Rule executed successfully without any issues. Note: this status is just an indication of a rule's "health". The rule might or might not generate any alerts despite of it.
-
- enum:
- - going to run
- - running
- - partial failure
- - failed
- - succeeded
-
- RuleExecutionResult:
- type: object
- description: |-
- Rule execution result is an aggregate that groups plain rule execution events by execution UUID.
- properties:
- execution_uuid:
- type: string
- timestamp:
- type: string
- format: date-time
- duration_ms:
- type: integer
- status:
- type: string
- message:
- type: string
- num_active_alerts:
- type: integer
- num_new_alerts:
- type: integer
- num_recovered_alerts:
- type: integer
- num_triggered_actions:
- type: integer
- num_succeeded_actions:
- type: integer
- num_errored_actions:
- type: integer
- total_search_duration_ms:
- type: integer
- es_search_duration_ms:
- type: integer
- schedule_delay_ms:
- type: integer
- timed_out:
- type: boolean
- indexing_duration_ms:
- type: integer
- search_duration_ms:
- type: integer
- gap_duration_s:
- type: integer
- security_status:
- type: string
- security_message:
- type: string
- required:
- - execution_uuid
- - timestamp
- - duration_ms
- - status
- - message
- - num_active_alerts
- - num_new_alerts
- - num_recovered_alerts
- - num_triggered_actions
- - num_succeeded_actions
- - num_errored_actions
- - total_search_duration_ms
- - es_search_duration_ms
- - schedule_delay_ms
- - timed_out
- - indexing_duration_ms
- - search_duration_ms
- - gap_duration_s
- - security_status
- - security_message
-
Action:
type: object
properties:
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/sorting.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/sorting.gen.ts
new file mode 100644
index 0000000000000..b2206c5a381ef
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/sorting.gen.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { z } from 'zod';
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ */
+
+export type SortOrder = z.infer;
+export const SortOrder = z.enum(['asc', 'desc']);
+export const SortOrderEnum = SortOrder.enum;
+export type SortOrderEnum = typeof SortOrder.enum;
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/sorting.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/sorting.schema.yaml
new file mode 100644
index 0000000000000..1b1893d19e4df
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/sorting.schema.yaml
@@ -0,0 +1,13 @@
+openapi: 3.0.0
+info:
+ title: Sorting Schema
+ version: 'not applicable'
+paths: {}
+components:
+ x-codegen-enabled: true
+ schemas:
+ SortOrder:
+ type: string
+ enum:
+ - asc
+ - desc
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/warning_schema.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/warning_schema.ts
index 1a401d1941cb0..9f82dc5db0605 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/model/warning_schema.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/warning_schema.ts
@@ -20,5 +20,5 @@ const required = t.exact(
})
);
-export const warningSchema = t.intersection([partial, required]);
-export type WarningSchema = t.TypeOf;
+export const WarningSchema = t.intersection([partial, required]);
+export type WarningSchema = t.TypeOf;
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts
index 87b43732bb6d2..a2b514676767b 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/index.ts
@@ -7,7 +7,7 @@
export * from './get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.gen';
export * from './get_prebuilt_rules_status/get_prebuilt_rules_status_route';
-export * from './install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route';
+export * from './install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.gen';
export * from './perform_rule_installation/perform_rule_installation_route';
export * from './perform_rule_upgrade/perform_rule_upgrade_route';
export * from './review_rule_installation/review_rule_installation_route';
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.gen.ts
new file mode 100644
index 0000000000000..f6cb7dec85143
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.gen.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { z } from 'zod';
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ */
+
+export type InstallPrebuiltRulesAndTimelinesResponse = z.infer<
+ typeof InstallPrebuiltRulesAndTimelinesResponse
+>;
+export const InstallPrebuiltRulesAndTimelinesResponse = z
+ .object({
+ /**
+ * The number of rules installed
+ */
+ rules_installed: z.number().int().min(0),
+ /**
+ * The number of rules updated
+ */
+ rules_updated: z.number().int().min(0),
+ /**
+ * The number of timelines installed
+ */
+ timelines_installed: z.number().int().min(0),
+ /**
+ * The number of timelines updated
+ */
+ timelines_updated: z.number().int().min(0),
+ })
+ .strict();
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.schema.yaml
index a7c2309d4a542..158b8667bb615 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.schema.yaml
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.schema.yaml
@@ -5,8 +5,8 @@ info:
paths:
/api/detection_engine/rules/prepackaged:
put:
- operationId: InstallPrebuiltRules
- x-codegen-enabled: false
+ operationId: InstallPrebuiltRulesAndTimelines
+ x-codegen-enabled: true
summary: Installs all Elastic prebuilt rules and timelines
tags:
- Prebuilt Rules API
@@ -17,6 +17,7 @@ paths:
application/json:
schema:
type: object
+ additionalProperties: false
properties:
rules_installed:
type: integer
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.test.ts
index 7f049af0d78b8..5edbd070b3972 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.test.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.test.ts
@@ -5,11 +5,9 @@
* 2.0.
*/
-import { left } from 'fp-ts/lib/Either';
-import { pipe } from 'fp-ts/lib/pipeable';
-import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
-
-import { InstallPrebuiltRulesAndTimelinesResponse } from './install_prebuilt_rules_and_timelines_route';
+import { stringifyZodError } from '@kbn/securitysolution-es-utils';
+import { expectParseError, expectParseSuccess } from '../../../../test/zod_helpers';
+import { InstallPrebuiltRulesAndTimelinesResponse } from './install_prebuilt_rules_and_timelines_route.gen';
describe('Install prebuilt rules and timelines response schema', () => {
test('it should validate an empty prepackaged response with defaults', () => {
@@ -19,12 +17,9 @@ describe('Install prebuilt rules and timelines response schema', () => {
timelines_installed: 0,
timelines_updated: 0,
};
- const decoded = InstallPrebuiltRulesAndTimelinesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(payload);
+ const result = InstallPrebuiltRulesAndTimelinesResponse.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(payload);
});
test('it should not validate an extra invalid field added', () => {
@@ -35,12 +30,11 @@ describe('Install prebuilt rules and timelines response schema', () => {
timelines_installed: 0,
timelines_updated: 0,
};
- const decoded = InstallPrebuiltRulesAndTimelinesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_field"']);
- expect(message.schema).toEqual({});
+ const result = InstallPrebuiltRulesAndTimelinesResponse.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(
+ "Unrecognized key(s) in object: 'invalid_field'"
+ );
});
test('it should NOT validate an empty prepackaged response with a negative "rules_installed" number', () => {
@@ -50,14 +44,11 @@ describe('Install prebuilt rules and timelines response schema', () => {
timelines_installed: 0,
timelines_updated: 0,
};
- const decoded = InstallPrebuiltRulesAndTimelinesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "-1" supplied to "rules_installed"',
- ]);
- expect(message.schema).toEqual({});
+ const result = InstallPrebuiltRulesAndTimelinesResponse.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(
+ 'rules_installed: Number must be greater than or equal to 0'
+ );
});
test('it should NOT validate an empty prepackaged response with a negative "rules_updated"', () => {
@@ -67,14 +58,11 @@ describe('Install prebuilt rules and timelines response schema', () => {
timelines_installed: 0,
timelines_updated: 0,
};
- const decoded = InstallPrebuiltRulesAndTimelinesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "-1" supplied to "rules_updated"',
- ]);
- expect(message.schema).toEqual({});
+ const result = InstallPrebuiltRulesAndTimelinesResponse.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(
+ 'rules_updated: Number must be greater than or equal to 0'
+ );
});
test('it should NOT validate an empty prepackaged response if "rules_installed" is not there', () => {
@@ -86,14 +74,9 @@ describe('Install prebuilt rules and timelines response schema', () => {
};
// @ts-expect-error
delete payload.rules_installed;
- const decoded = InstallPrebuiltRulesAndTimelinesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "undefined" supplied to "rules_installed"',
- ]);
- expect(message.schema).toEqual({});
+ const result = InstallPrebuiltRulesAndTimelinesResponse.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual('rules_installed: Required');
});
test('it should NOT validate an empty prepackaged response if "rules_updated" is not there', () => {
@@ -105,13 +88,8 @@ describe('Install prebuilt rules and timelines response schema', () => {
};
// @ts-expect-error
delete payload.rules_updated;
- const decoded = InstallPrebuiltRulesAndTimelinesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "undefined" supplied to "rules_updated"',
- ]);
- expect(message.schema).toEqual({});
+ const result = InstallPrebuiltRulesAndTimelinesResponse.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual('rules_updated: Required');
});
});
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts
deleted file mode 100644
index 7da2d6b1fae03..0000000000000
--- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts
+++ /dev/null
@@ -1,22 +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 * as t from 'io-ts';
-import { PositiveInteger } from '@kbn/securitysolution-io-ts-types';
-
-export type InstallPrebuiltRulesAndTimelinesResponse = t.TypeOf<
- typeof InstallPrebuiltRulesAndTimelinesResponse
->;
-export const InstallPrebuiltRulesAndTimelinesResponse = t.exact(
- t.type({
- rules_installed: PositiveInteger,
- rules_updated: PositiveInteger,
-
- timelines_installed: PositiveInteger,
- timelines_updated: PositiveInteger,
- })
-);
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/response_schema.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/response_schema.ts
index b6f9bb359344b..07bef679e41ac 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/response_schema.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/response_schema.ts
@@ -7,7 +7,7 @@
import * as t from 'io-ts';
-import { RuleResponse, errorSchema } from '../../model';
+import { RuleResponse, ErrorSchema } from '../../model';
export type BulkCrudRulesResponse = t.TypeOf;
-export const BulkCrudRulesResponse = t.array(t.union([RuleResponse, errorSchema]));
+export const BulkCrudRulesResponse = t.array(t.union([RuleResponse, ErrorSchema]));
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/export_rules/export_rules_route.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/export_rules/export_rules_route.gen.ts
new file mode 100644
index 0000000000000..01cd91216753f
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/export_rules/export_rules_route.gen.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { z } from 'zod';
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ */
+
+import { RuleSignatureId } from '../../model/rule_schema/common_attributes.gen';
+
+export type ExportRulesRequestQuery = z.infer;
+export const ExportRulesRequestQuery = z.object({
+ /**
+ * Determines whether a summary of the exported rules is returned.
+ */
+ exclude_export_details: z.preprocess(
+ (value: unknown) => (typeof value === 'boolean' ? String(value) : value),
+ z
+ .enum(['true', 'false'])
+ .default('false')
+ .transform((value) => value === 'true')
+ ),
+ /**
+ * File name for saving the exported rules.
+ */
+ file_name: z.string().optional().default('export.ndjson'),
+});
+export type ExportRulesRequestQueryInput = z.input;
+
+export type ExportRulesRequestBody = z.infer;
+export const ExportRulesRequestBody = z
+ .object({
+ /**
+ * Array of `rule_id` fields. Exports all rules when unspecified.
+ */
+ objects: z.array(
+ z.object({
+ rule_id: RuleSignatureId,
+ })
+ ),
+ })
+ .nullable();
+export type ExportRulesRequestBodyInput = z.input;
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/export_rules/export_rules_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/export_rules/export_rules_route.schema.yaml
index cc15d750c9a31..5b36290ddf174 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/export_rules/export_rules_route.schema.yaml
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/export_rules/export_rules_route.schema.yaml
@@ -7,7 +7,7 @@ paths:
summary: Exports rules to an `.ndjson` file
post:
operationId: ExportRules
- x-codegen-enabled: false
+ x-codegen-enabled: true
summary: Export rules
description: Exports rules to an `.ndjson` file. The following configuration items are also included in the `.ndjson` file - Actions, Exception lists. Prebuilt rules cannot be exported.
tags:
@@ -35,6 +35,7 @@ paths:
type: object
required:
- objects
+ nullable: true
properties:
objects:
type: array
@@ -44,13 +45,13 @@ paths:
- rule_id
properties:
rule_id:
- type: string
+ $ref: '../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/RuleSignatureId'
description: Array of `rule_id` fields. Exports all rules when unspecified.
responses:
200:
description: Indicates a successful call.
content:
- application/json:
+ application/ndjson:
schema:
type: string
format: binary
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/export_rules/export_rules_route.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/export_rules/export_rules_route.test.ts
index 86dea6c90fb46..2ec18f7f86c8b 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/export_rules/export_rules_route.test.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/export_rules/export_rules_route.test.ts
@@ -5,55 +5,43 @@
* 2.0.
*/
-import { left } from 'fp-ts/lib/Either';
-import { pipe } from 'fp-ts/lib/pipeable';
-import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
-
-import { ExportRulesRequestBody, ExportRulesRequestQuery } from './export_rules_route';
+import { stringifyZodError } from '@kbn/securitysolution-es-utils';
+import { expectParseError, expectParseSuccess } from '../../../../test/zod_helpers';
+import type { ExportRulesRequestQueryInput } from './export_rules_route.gen';
+import { ExportRulesRequestBody, ExportRulesRequestQuery } from './export_rules_route.gen';
describe('Export rules request schema', () => {
describe('ExportRulesRequestBody', () => {
test('null value or absent values validate', () => {
const payload: Partial = null;
- const decoded = ExportRulesRequestBody.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(payload);
+ const result = ExportRulesRequestBody.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(payload);
});
test('empty object does not validate', () => {
const payload = {};
- const decoded = ExportRulesRequestBody.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "undefined" supplied to "objects"',
- 'Invalid value "{}" supplied to "({| objects: Array<{| rule_id: string |}> |} | null)"',
- ]);
- expect(message.schema).toEqual(payload);
+ const result = ExportRulesRequestBody.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual('objects: Required');
});
test('empty object array does validate', () => {
const payload: ExportRulesRequestBody = { objects: [] };
- const decoded = ExportRulesRequestBody.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(payload);
+ const result = ExportRulesRequestBody.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(payload);
});
test('array with rule_id validates', () => {
const payload: ExportRulesRequestBody = { objects: [{ rule_id: 'test-1' }] };
- const decoded = ExportRulesRequestBody.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(payload);
+ const result = ExportRulesRequestBody.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(payload);
});
test('array with id does not validate as we do not allow that on purpose since we export rule_id', () => {
@@ -61,94 +49,80 @@ describe('Export rules request schema', () => {
objects: [{ id: '4a7ff83d-3055-4bb2-ba68-587b9c6c15a4' }],
};
- const decoded = ExportRulesRequestBody.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "undefined" supplied to "objects,rule_id"',
- 'Invalid value "{"objects":[{"id":"4a7ff83d-3055-4bb2-ba68-587b9c6c15a4"}]}" supplied to "({| objects: Array<{| rule_id: string |}> |} | null)"',
- ]);
- expect(message.schema).toEqual({});
+ const result = ExportRulesRequestBody.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual('objects.0.rule_id: Required');
});
});
describe('ExportRulesRequestQuery', () => {
test('default value for file_name is export.ndjson and default for exclude_export_details is false', () => {
- const payload: Partial = {};
+ const payload: ExportRulesRequestQueryInput = {};
- const decoded = ExportRulesRequestQuery.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
- expect(getPaths(left(message.errors))).toEqual([]);
const expected: ExportRulesRequestQuery = {
file_name: 'export.ndjson',
exclude_export_details: false,
};
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(expected);
+
+ const result = ExportRulesRequestQuery.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(expected);
});
test('file_name validates', () => {
- const payload: ExportRulesRequestQuery = {
+ const payload: ExportRulesRequestQueryInput = {
file_name: 'test.ndjson',
};
- const decoded = ExportRulesRequestQuery.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
- expect(getPaths(left(message.errors))).toEqual([]);
const expected: ExportRulesRequestQuery = {
file_name: 'test.ndjson',
exclude_export_details: false,
};
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(expected);
+
+ const result = ExportRulesRequestQuery.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(expected);
});
test('file_name does not validate with a number', () => {
- const payload: Omit & { file_name: number } = {
+ const payload: Omit & { file_name: number } = {
file_name: 10,
};
- const decoded = ExportRulesRequestQuery.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "10" supplied to "file_name"',
- ]);
- expect(message.schema).toEqual({});
+ const result = ExportRulesRequestQuery.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(
+ 'file_name: Expected string, received number'
+ );
});
test('exclude_export_details validates with a boolean true', () => {
- const payload: ExportRulesRequestQuery = {
+ const payload: ExportRulesRequestQueryInput = {
exclude_export_details: true,
};
- const decoded = ExportRulesRequestQuery.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
- expect(getPaths(left(message.errors))).toEqual([]);
const expected: ExportRulesRequestQuery = {
exclude_export_details: true,
file_name: 'export.ndjson',
};
- expect(message.schema).toEqual(expected);
+
+ const result = ExportRulesRequestQuery.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(expected);
});
test('exclude_export_details does not validate with a string', () => {
- const payload: Omit & {
+ const payload: Omit & {
exclude_export_details: string;
} = {
exclude_export_details: 'invalid string',
};
- const decoded = ExportRulesRequestQuery.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "invalid string" supplied to "exclude_export_details"',
- ]);
- expect(message.schema).toEqual({});
+ const result = ExportRulesRequestQuery.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(
+ `exclude_export_details: Invalid enum value. Expected 'true' | 'false', received 'invalid string'`
+ );
});
});
});
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/export_rules/export_rules_route.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/export_rules/export_rules_route.ts
deleted file mode 100644
index f4746d3e090da..0000000000000
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/export_rules/export_rules_route.ts
+++ /dev/null
@@ -1,31 +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 * as t from 'io-ts';
-import { DefaultExportFileName } from '@kbn/securitysolution-io-ts-alerting-types';
-import { DefaultStringBooleanFalse } from '@kbn/securitysolution-io-ts-types';
-
-import { RuleSignatureId } from '../../model';
-
-const ObjectsWithRuleId = t.array(t.exact(t.type({ rule_id: RuleSignatureId })));
-
-/**
- * Request body parameters of the API route.
- */
-export type ExportRulesRequestBody = t.TypeOf;
-export const ExportRulesRequestBody = t.union([
- t.exact(t.type({ objects: ObjectsWithRuleId })),
- t.null,
-]);
-
-/**
- * Query string parameters of the API route.
- */
-export type ExportRulesRequestQuery = t.TypeOf;
-export const ExportRulesRequestQuery = t.exact(
- t.partial({ file_name: DefaultExportFileName, exclude_export_details: DefaultStringBooleanFalse })
-);
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/import_rules_route.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/import_rules_route.gen.ts
new file mode 100644
index 0000000000000..d0a105e28c2c8
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/import_rules_route.gen.ts
@@ -0,0 +1,78 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { z } from 'zod';
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ */
+
+import { ErrorSchema } from '../../model/error_schema.gen';
+import { WarningSchema } from '../../model/warning_schema.gen';
+
+export type ImportRulesRequestQuery = z.infer;
+export const ImportRulesRequestQuery = z.object({
+ /**
+ * Determines whether existing rules with the same `rule_id` are overwritten.
+ */
+ overwrite: z.preprocess(
+ (value: unknown) => (typeof value === 'boolean' ? String(value) : value),
+ z
+ .enum(['true', 'false'])
+ .default('false')
+ .transform((value) => value === 'true')
+ ),
+ /**
+ * Determines whether existing exception lists with the same `list_id` are overwritten.
+ */
+ overwrite_exceptions: z.preprocess(
+ (value: unknown) => (typeof value === 'boolean' ? String(value) : value),
+ z
+ .enum(['true', 'false'])
+ .default('false')
+ .transform((value) => value === 'true')
+ ),
+ /**
+ * Determines whether existing actions with the same `kibana.alert.rule.actions.id` are overwritten.
+ */
+ overwrite_action_connectors: z.preprocess(
+ (value: unknown) => (typeof value === 'boolean' ? String(value) : value),
+ z
+ .enum(['true', 'false'])
+ .default('false')
+ .transform((value) => value === 'true')
+ ),
+ /**
+ * Generates a new list ID for each imported exception list.
+ */
+ as_new_list: z.preprocess(
+ (value: unknown) => (typeof value === 'boolean' ? String(value) : value),
+ z
+ .enum(['true', 'false'])
+ .default('false')
+ .transform((value) => value === 'true')
+ ),
+});
+export type ImportRulesRequestQueryInput = z.input;
+
+export type ImportRulesResponse = z.infer;
+export const ImportRulesResponse = z
+ .object({
+ exceptions_success: z.boolean(),
+ exceptions_success_count: z.number().int().min(0),
+ exceptions_errors: z.array(ErrorSchema),
+ rules_count: z.number().int().min(0),
+ success: z.boolean(),
+ success_count: z.number().int().min(0),
+ errors: z.array(ErrorSchema),
+ action_connectors_errors: z.array(ErrorSchema),
+ action_connectors_warnings: z.array(WarningSchema),
+ action_connectors_success: z.boolean(),
+ action_connectors_success_count: z.number().int().min(0),
+ })
+ .strict();
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/import_rules_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/import_rules_route.schema.yaml
index b6879c7ace5e6..e158434354fde 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/import_rules_route.schema.yaml
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/import_rules_route.schema.yaml
@@ -7,7 +7,7 @@ paths:
summary: Imports rules from an `.ndjson` file
post:
operationId: ImportRules
- x-codegen-enabled: false
+ x-codegen-enabled: true
summary: Import rules
description: Imports rules from an `.ndjson` file, including actions and exception lists.
tags:
@@ -45,6 +45,13 @@ paths:
schema:
type: boolean
default: false
+ - name: as_new_list
+ in: query
+ required: false
+ description: Generates a new list ID for each imported exception list.
+ schema:
+ type: boolean
+ default: false
responses:
200:
description: Indicates a successful call.
@@ -52,3 +59,51 @@ paths:
application/json:
schema:
type: object
+ additionalProperties: false
+ required:
+ - exceptions_success
+ - exceptions_success_count
+ - exceptions_errors
+ - rules_count
+ - success
+ - success_count
+ - errors
+ - action_connectors_errors
+ - action_connectors_warnings
+ - action_connectors_success
+ - action_connectors_success_count
+ properties:
+ exceptions_success:
+ type: boolean
+ exceptions_success_count:
+ type: integer
+ minimum: 0
+ exceptions_errors:
+ type: array
+ items:
+ $ref: '../../model/error_schema.schema.yaml#/components/schemas/ErrorSchema'
+ rules_count:
+ type: integer
+ minimum: 0
+ success:
+ type: boolean
+ success_count:
+ type: integer
+ minimum: 0
+ errors:
+ type: array
+ items:
+ $ref: '../../model/error_schema.schema.yaml#/components/schemas/ErrorSchema'
+ action_connectors_errors:
+ type: array
+ items:
+ $ref: '../../model/error_schema.schema.yaml#/components/schemas/ErrorSchema'
+ action_connectors_warnings:
+ type: array
+ items:
+ $ref: '../../model/warning_schema.schema.yaml#/components/schemas/WarningSchema'
+ action_connectors_success:
+ type: boolean
+ action_connectors_success_count:
+ type: integer
+ minimum: 0
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/import_rules_route.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/import_rules_route.test.ts
index 64630dca05610..56fcece0f122a 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/import_rules_route.test.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/import_rules_route.test.ts
@@ -5,15 +5,10 @@
* 2.0.
*/
-import { pipe } from 'fp-ts/lib/pipeable';
-import type { Either } from 'fp-ts/lib/Either';
-import { left } from 'fp-ts/lib/Either';
-import type { Errors } from 'io-ts';
-
-import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
-import type { ErrorSchema } from '../../model/error_schema';
-
-import { ImportRulesResponse } from './import_rules_route';
+import { stringifyZodError } from '@kbn/securitysolution-es-utils';
+import { expectParseError, expectParseSuccess } from '../../../../test/zod_helpers';
+import type { ErrorSchema } from '../../model/error_schema.gen';
+import { ImportRulesResponse } from './import_rules_route.gen';
describe('Import rules schema', () => {
describe('response schema', () => {
@@ -31,12 +26,9 @@ describe('Import rules schema', () => {
action_connectors_errors: [],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(payload);
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(payload);
});
test('it should validate an empty import response with a single error', () => {
@@ -53,12 +45,9 @@ describe('Import rules schema', () => {
action_connectors_errors: [],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(payload);
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(payload);
});
test('it should validate an empty import response with a single exceptions error', () => {
@@ -75,12 +64,9 @@ describe('Import rules schema', () => {
action_connectors_errors: [],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(payload);
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(payload);
});
test('it should validate an empty import response with two errors', () => {
@@ -100,12 +86,9 @@ describe('Import rules schema', () => {
action_connectors_errors: [],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(payload);
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(payload);
});
test('it should validate an empty import response with two exception errors', () => {
@@ -125,12 +108,9 @@ describe('Import rules schema', () => {
action_connectors_errors: [],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(payload);
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(payload);
});
test('it should NOT validate a success_count that is a negative number', () => {
@@ -147,14 +127,11 @@ describe('Import rules schema', () => {
action_connectors_errors: [],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "-1" supplied to "success_count"',
- ]);
- expect(message.schema).toEqual({});
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(
+ 'success_count: Number must be greater than or equal to 0'
+ );
});
test('it should NOT validate a exceptions_success_count that is a negative number', () => {
@@ -171,35 +148,14 @@ describe('Import rules schema', () => {
action_connectors_errors: [],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "-1" supplied to "exceptions_success_count"',
- ]);
- expect(message.schema).toEqual({});
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(
+ 'exceptions_success_count: Number must be greater than or equal to 0'
+ );
});
test('it should NOT validate a success that is not a boolean', () => {
- type UnsafeCastForTest = Either<
- Errors,
- {
- success: string;
- success_count: number;
- errors: Array<
- {
- id?: string | undefined;
- rule_id?: string | undefined;
- } & {
- error: {
- status_code: number;
- message: string;
- };
- }
- >;
- }
- >;
const payload: Omit & { success: string } = {
success: 'hello',
success_count: 0,
@@ -213,36 +169,12 @@ describe('Import rules schema', () => {
action_connectors_errors: [],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded as UnsafeCastForTest);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "hello" supplied to "success"',
- ]);
- expect(message.schema).toEqual({});
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual('success: Expected boolean, received string');
});
test('it should NOT validate a exceptions_success that is not a boolean', () => {
- type UnsafeCastForTest = Either<
- Errors,
- {
- success: boolean;
- exceptions_success: string;
- success_count: number;
- errors: Array<
- {
- id?: string | undefined;
- rule_id?: string | undefined;
- } & {
- error: {
- status_code: number;
- message: string;
- };
- }
- >;
- }
- >;
const payload: Omit & {
exceptions_success: string;
} = {
@@ -258,14 +190,11 @@ describe('Import rules schema', () => {
action_connectors_errors: [],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded as UnsafeCastForTest);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "hello" supplied to "exceptions_success"',
- ]);
- expect(message.schema).toEqual({});
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(
+ 'exceptions_success: Expected boolean, received string'
+ );
});
test('it should NOT validate a success an extra invalid field', () => {
@@ -283,12 +212,11 @@ describe('Import rules schema', () => {
action_connectors_errors: [],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_field"']);
- expect(message.schema).toEqual({});
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(
+ "Unrecognized key(s) in object: 'invalid_field'"
+ );
});
test('it should NOT validate an extra field in the second position of the errors array', () => {
@@ -311,12 +239,11 @@ describe('Import rules schema', () => {
action_connectors_errors: [],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual(['invalid keys "invalid_data"']);
- expect(message.schema).toEqual({});
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(
+ "errors.1: Unrecognized key(s) in object: 'invalid_data'"
+ );
});
test('it should validate an empty import response with a single connectors error', () => {
@@ -333,13 +260,11 @@ describe('Import rules schema', () => {
action_connectors_errors: [{ error: { status_code: 400, message: 'some message' } }],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(payload);
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(payload);
});
+
test('it should validate an empty import response with multiple errors', () => {
const payload: ImportRulesResponse = {
success: false,
@@ -357,33 +282,12 @@ describe('Import rules schema', () => {
action_connectors_errors: [{ error: { status_code: 400, message: 'some message' } }],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(payload);
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(payload);
});
+
test('it should NOT validate action_connectors_success that is not boolean', () => {
- type UnsafeCastForTest = Either<
- Errors,
- {
- success: boolean;
- action_connectors_success: string;
- success_count: number;
- errors: Array<
- {
- id?: string | undefined;
- rule_id?: string | undefined;
- } & {
- error: {
- status_code: number;
- message: string;
- };
- }
- >;
- }
- >;
const payload: Omit & {
action_connectors_success: string;
} = {
@@ -399,15 +303,13 @@ describe('Import rules schema', () => {
action_connectors_errors: [],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded as UnsafeCastForTest);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "invalid" supplied to "action_connectors_success"',
- ]);
- expect(message.schema).toEqual({});
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(
+ 'action_connectors_success: Expected boolean, received string'
+ );
});
+
test('it should NOT validate a action_connectors_success_count that is a negative number', () => {
const payload: ImportRulesResponse = {
success: false,
@@ -422,16 +324,13 @@ describe('Import rules schema', () => {
action_connectors_errors: [],
action_connectors_warnings: [],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "-1" supplied to "action_connectors_success_count"',
- ]);
- expect(message.schema).toEqual({});
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(
+ 'action_connectors_success_count: Number must be greater than or equal to 0'
+ );
});
- test('it should validate a action_connectors_warnings after importing successfully', () => {
+ test('it should validate a action_connectors_warnings after importing successfully', () => {
const payload: ImportRulesResponse = {
success: false,
success_count: 0,
@@ -447,33 +346,12 @@ describe('Import rules schema', () => {
{ type: 'type', message: 'message', actionPath: 'actionPath' },
],
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(payload);
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(payload);
});
+
test('it should NOT validate a action_connectors_warnings that is not WarningSchema', () => {
- type UnsafeCastForTest = Either<
- Errors,
- {
- success: boolean;
- action_connectors_warnings: string;
- success_count: number;
- errors: Array<
- {
- id?: string | undefined;
- rule_id?: string | undefined;
- } & {
- error: {
- status_code: number;
- message: string;
- };
- }
- >;
- }
- >;
const payload: Omit & {
action_connectors_warnings: string;
} = {
@@ -489,14 +367,11 @@ describe('Import rules schema', () => {
action_connectors_errors: [],
action_connectors_warnings: 'invalid',
};
- const decoded = ImportRulesResponse.decode(payload);
- const checked = exactCheck(payload, decoded as UnsafeCastForTest);
- const message = pipe(checked, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "invalid" supplied to "action_connectors_warnings"',
- ]);
- expect(message.schema).toEqual({});
+ const result = ImportRulesResponse.safeParse(payload);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(
+ 'action_connectors_warnings: Expected array, received string'
+ );
});
});
});
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/import_rules_route.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/import_rules_route.ts
deleted file mode 100644
index 8a427f16fd8c6..0000000000000
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/import_rules_route.ts
+++ /dev/null
@@ -1,44 +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 * as t from 'io-ts';
-import { DefaultStringBooleanFalse, PositiveInteger } from '@kbn/securitysolution-io-ts-types';
-import { errorSchema, warningSchema } from '../../model';
-
-export const ImportRulesRequestQuery = t.exact(
- t.partial({
- overwrite: DefaultStringBooleanFalse,
- overwrite_exceptions: DefaultStringBooleanFalse,
- overwrite_action_connectors: DefaultStringBooleanFalse,
- as_new_list: DefaultStringBooleanFalse,
- })
-);
-
-export type ImportRulesRequestQuery = t.TypeOf;
-export interface ImportRulesRequestQueryDecoded {
- overwrite: boolean;
- overwrite_exceptions: boolean;
- overwrite_action_connectors: boolean;
- as_new_list: boolean;
-}
-
-export type ImportRulesResponse = t.TypeOf;
-export const ImportRulesResponse = t.exact(
- t.type({
- exceptions_success: t.boolean,
- exceptions_success_count: PositiveInteger,
- exceptions_errors: t.array(errorSchema),
- rules_count: PositiveInteger,
- success: t.boolean,
- success_count: PositiveInteger,
- errors: t.array(errorSchema),
- action_connectors_errors: t.array(errorSchema),
- action_connectors_warnings: t.array(warningSchema),
- action_connectors_success: t.boolean,
- action_connectors_success_count: PositiveInteger,
- })
-);
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/index.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/index.ts
index bfb1d0d16a34e..a9100970ca753 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/index.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/index.ts
@@ -21,14 +21,14 @@ export * from './crud/read_rule/read_rule_route';
export * from './crud/update_rule/request_schema_validation';
export * from './crud/update_rule/update_rule_route';
export * from './export_rules/export_rules_details_schema';
-export * from './export_rules/export_rules_route';
+export * from './export_rules/export_rules_route.gen';
export * from './find_rules/find_rules_route';
export * from './find_rules/request_schema_validation';
export * from './get_rule_management_filters/get_rule_management_filters_route';
-export * from './import_rules/import_rules_route';
+export * from './import_rules/import_rules_route.gen';
export * from './import_rules/rule_to_import_validation';
export * from './import_rules/rule_to_import';
export * from './model/query_rule_by_ids_validation';
export * from './model/query_rule_by_ids';
export * from './urls';
-export * from './read_tags/read_tags_route';
+export * from './read_tags/read_tags_route.gen';
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/read_tags/read_tags_route.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/read_tags/read_tags_route.gen.ts
new file mode 100644
index 0000000000000..a2a52f4a1f7e9
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/read_tags/read_tags_route.gen.ts
@@ -0,0 +1,18 @@
+/*
+ * 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 { z } from 'zod';
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ */
+
+import { RuleTagArray } from '../../model/rule_schema/common_attributes.gen';
+
+export type ReadTagsResponse = z.infer;
+export const ReadTagsResponse = RuleTagArray;
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/read_tags/read_tags_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/read_tags/read_tags_route.schema.yaml
index 5f4da0a62fcea..b9e79f252a269 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/read_tags/read_tags_route.schema.yaml
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/read_tags/read_tags_route.schema.yaml
@@ -6,8 +6,8 @@ paths:
/api/detection_engine/tags:
summary: Aggregates and returns rule tags
get:
- operationId: GetTags
- x-codegen-enabled: false
+ operationId: ReadTags
+ x-codegen-enabled: true
summary: Aggregates and returns all unique tags from all rules
tags:
- Tags API
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/index.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/index.ts
index b23603ef8084e..1494e09b9c51a 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/index.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/index.ts
@@ -11,13 +11,14 @@ export * from './detection_engine_health/get_space_health/get_space_health_route
export * from './detection_engine_health/setup_health/setup_health_route';
export * from './detection_engine_health/model';
export * from './rule_execution_logs/get_rule_execution_events/get_rule_execution_events_route';
-export * from './rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route';
+export * from './rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.gen';
export * from './urls';
export * from './model/execution_event';
export * from './model/execution_metrics';
-export * from './model/execution_result';
+export * from './model/execution_result.gen';
export * from './model/execution_settings';
+export * from './model/execution_status.gen';
export * from './model/execution_status';
export * from './model/execution_summary';
export * from './model/log_level';
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_result.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_result.gen.ts
new file mode 100644
index 0000000000000..41eab285ac3f0
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_result.gen.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { z } from 'zod';
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ */
+
+/**
+ * Rule execution result is an aggregate that groups plain rule execution events by execution UUID. It contains such information as execution UUID, date, status and metrics.
+ */
+export type RuleExecutionResult = z.infer;
+export const RuleExecutionResult = z.object({
+ execution_uuid: z.string(),
+ timestamp: z.string().datetime(),
+ duration_ms: z.number().int(),
+ status: z.string(),
+ message: z.string(),
+ num_active_alerts: z.number().int(),
+ num_new_alerts: z.number().int(),
+ num_recovered_alerts: z.number().int(),
+ num_triggered_actions: z.number().int(),
+ num_succeeded_actions: z.number().int(),
+ num_errored_actions: z.number().int(),
+ total_search_duration_ms: z.number().int(),
+ es_search_duration_ms: z.number().int(),
+ schedule_delay_ms: z.number().int(),
+ timed_out: z.boolean(),
+ indexing_duration_ms: z.number().int(),
+ search_duration_ms: z.number().int(),
+ gap_duration_s: z.number().int(),
+ security_status: z.string(),
+ security_message: z.string(),
+});
+
+/**
+ * We support sorting rule execution results by these fields.
+ */
+export type SortFieldOfRuleExecutionResult = z.infer;
+export const SortFieldOfRuleExecutionResult = z.enum([
+ 'timestamp',
+ 'duration_ms',
+ 'gap_duration_s',
+ 'indexing_duration_ms',
+ 'search_duration_ms',
+ 'schedule_delay_ms',
+]);
+export const SortFieldOfRuleExecutionResultEnum = SortFieldOfRuleExecutionResult.enum;
+export type SortFieldOfRuleExecutionResultEnum = typeof SortFieldOfRuleExecutionResult.enum;
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_result.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_result.mock.ts
index 4a039ca949c82..4016d02ec4ebc 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_result.mock.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_result.mock.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import type { RuleExecutionResult } from './execution_result';
+import type { RuleExecutionResult } from './execution_result.gen';
const getSomeResults = (): RuleExecutionResult[] => [
{
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_result.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_result.schema.yaml
new file mode 100644
index 0000000000000..3c8d91d3b9d7f
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_result.schema.yaml
@@ -0,0 +1,86 @@
+openapi: 3.0.0
+info:
+ title: Execution Result Schema
+ version: not applicable
+paths: {}
+components:
+ x-codegen-enabled: true
+ schemas:
+ RuleExecutionResult:
+ type: object
+ description: |-
+ Rule execution result is an aggregate that groups plain rule execution events by execution UUID. It contains such information as execution UUID, date, status and metrics.
+ properties:
+ execution_uuid:
+ type: string
+ timestamp:
+ type: string
+ format: date-time
+ duration_ms:
+ type: integer
+ status:
+ type: string
+ message:
+ type: string
+ num_active_alerts:
+ type: integer
+ num_new_alerts:
+ type: integer
+ num_recovered_alerts:
+ type: integer
+ num_triggered_actions:
+ type: integer
+ num_succeeded_actions:
+ type: integer
+ num_errored_actions:
+ type: integer
+ total_search_duration_ms:
+ type: integer
+ es_search_duration_ms:
+ type: integer
+ schedule_delay_ms:
+ type: integer
+ timed_out:
+ type: boolean
+ indexing_duration_ms:
+ type: integer
+ search_duration_ms:
+ type: integer
+ gap_duration_s:
+ type: integer
+ security_status:
+ type: string
+ security_message:
+ type: string
+ required:
+ - execution_uuid
+ - timestamp
+ - duration_ms
+ - status
+ - message
+ - num_active_alerts
+ - num_new_alerts
+ - num_recovered_alerts
+ - num_triggered_actions
+ - num_succeeded_actions
+ - num_errored_actions
+ - total_search_duration_ms
+ - es_search_duration_ms
+ - schedule_delay_ms
+ - timed_out
+ - indexing_duration_ms
+ - search_duration_ms
+ - gap_duration_s
+ - security_status
+ - security_message
+
+ SortFieldOfRuleExecutionResult:
+ type: string
+ description: We support sorting rule execution results by these fields.
+ enum:
+ - timestamp
+ - duration_ms
+ - gap_duration_s
+ - indexing_duration_ms
+ - search_duration_ms
+ - schedule_delay_ms
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_status.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_status.gen.ts
new file mode 100644
index 0000000000000..2357e95dae817
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_status.gen.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 { z } from 'zod';
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ */
+
+/**
+ * Custom execution status of Security rules that is different from the status used in the Alerting Framework. We merge our custom status with the Framework's status to determine the resulting status of a rule.
+- going to run - @deprecated Replaced by the 'running' status but left for backwards compatibility with rule execution events already written to Event Log in the prior versions of Kibana. Don't use when writing rule status changes.
+- running - Rule execution started but not reached any intermediate or final status.
+- partial failure - Rule can partially fail for various reasons either in the middle of an execution (in this case we update its status right away) or in the end of it. So currently this status can be both intermediate and final at the same time. A typical reason for a partial failure: not all the indices that the rule searches over actually exist.
+- failed - Rule failed to execute due to unhandled exception or a reason defined in the business logic of its executor function.
+- succeeded - Rule executed successfully without any issues. Note: this status is just an indication of a rule's "health". The rule might or might not generate any alerts despite of it.
+ */
+export type RuleExecutionStatus = z.infer;
+export const RuleExecutionStatus = z.enum([
+ 'going to run',
+ 'running',
+ 'partial failure',
+ 'failed',
+ 'succeeded',
+]);
+export const RuleExecutionStatusEnum = RuleExecutionStatus.enum;
+export type RuleExecutionStatusEnum = typeof RuleExecutionStatus.enum;
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_status.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_status.schema.yaml
new file mode 100644
index 0000000000000..7675b21786188
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_status.schema.yaml
@@ -0,0 +1,24 @@
+openapi: 3.0.0
+info:
+ title: Execution Status Schema
+ version: not applicable
+paths: {}
+components:
+ x-codegen-enabled: true
+ schemas:
+ RuleExecutionStatus:
+ type: string
+ description: |-
+ Custom execution status of Security rules that is different from the status used in the Alerting Framework. We merge our custom status with the Framework's status to determine the resulting status of a rule.
+ - going to run - @deprecated Replaced by the 'running' status but left for backwards compatibility with rule execution events already written to Event Log in the prior versions of Kibana. Don't use when writing rule status changes.
+ - running - Rule execution started but not reached any intermediate or final status.
+ - partial failure - Rule can partially fail for various reasons either in the middle of an execution (in this case we update its status right away) or in the end of it. So currently this status can be both intermediate and final at the same time. A typical reason for a partial failure: not all the indices that the rule searches over actually exist.
+ - failed - Rule failed to execute due to unhandled exception or a reason defined in the business logic of its executor function.
+ - succeeded - Rule executed successfully without any issues. Note: this status is just an indication of a rule's "health". The rule might or might not generate any alerts despite of it.
+
+ enum:
+ - going to run
+ - running
+ - partial failure
+ - failed
+ - succeeded
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_status.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_status.ts
index 1e95e3a812054..c168cdc837fc3 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_status.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_status.ts
@@ -5,57 +5,15 @@
* 2.0.
*/
-import type * as t from 'io-ts';
-import { enumeration, PositiveInteger } from '@kbn/securitysolution-io-ts-types';
import type { RuleLastRunOutcomes } from '@kbn/alerting-plugin/common';
+import { enumeration, PositiveInteger } from '@kbn/securitysolution-io-ts-types';
+import type * as t from 'io-ts';
import { assertUnreachable } from '../../../../utility_types';
+import type { RuleExecutionStatus } from './execution_status.gen';
+import { RuleExecutionStatusEnum } from './execution_status.gen';
-/**
- * Custom execution status of Security rules that is different from the status
- * used in the Alerting Framework. We merge our custom status with the
- * Framework's status to determine the resulting status of a rule.
- */
-export enum RuleExecutionStatus {
- /**
- * @deprecated Replaced by the 'running' status but left for backwards compatibility
- * with rule execution events already written to Event Log in the prior versions of Kibana.
- * Don't use when writing rule status changes.
- */
- 'going to run' = 'going to run',
-
- /**
- * Rule execution started but not reached any intermediate or final status.
- */
- 'running' = 'running',
-
- /**
- * Rule can partially fail for various reasons either in the middle of an execution
- * (in this case we update its status right away) or in the end of it. So currently
- * this status can be both intermediate and final at the same time.
- * A typical reason for a partial failure: not all the indices that the rule searches
- * over actually exist.
- */
- 'partial failure' = 'partial failure',
-
- /**
- * Rule failed to execute due to unhandled exception or a reason defined in the
- * business logic of its executor function.
- */
- 'failed' = 'failed',
-
- /**
- * Rule executed successfully without any issues. Note: this status is just an indication
- * of a rule's "health". The rule might or might not generate any alerts despite of it.
- */
- 'succeeded' = 'succeeded',
-}
-
-export const TRuleExecutionStatus = enumeration('RuleExecutionStatus', RuleExecutionStatus);
-
-/**
- * An array of supported rule execution statuses.
- */
-export const RULE_EXECUTION_STATUSES = Object.values(RuleExecutionStatus);
+// TODO remove after the migration to Zod is done
+export const TRuleExecutionStatus = enumeration('RuleExecutionStatus', RuleExecutionStatusEnum);
export type RuleExecutionStatusOrder = t.TypeOf;
export const RuleExecutionStatusOrder = PositiveInteger;
@@ -64,15 +22,15 @@ export const ruleExecutionStatusToNumber = (
status: RuleExecutionStatus
): RuleExecutionStatusOrder => {
switch (status) {
- case RuleExecutionStatus.succeeded:
+ case RuleExecutionStatusEnum.succeeded:
return 0;
- case RuleExecutionStatus['going to run']:
+ case RuleExecutionStatusEnum['going to run']:
return 10;
- case RuleExecutionStatus.running:
+ case RuleExecutionStatusEnum.running:
return 15;
- case RuleExecutionStatus['partial failure']:
+ case RuleExecutionStatusEnum['partial failure']:
return 20;
- case RuleExecutionStatus.failed:
+ case RuleExecutionStatusEnum.failed:
return 30;
default:
assertUnreachable(status);
@@ -85,13 +43,13 @@ export const ruleLastRunOutcomeToExecutionStatus = (
): RuleExecutionStatus => {
switch (outcome) {
case 'succeeded':
- return RuleExecutionStatus.succeeded;
+ return RuleExecutionStatusEnum.succeeded;
case 'warning':
- return RuleExecutionStatus['partial failure'];
+ return RuleExecutionStatusEnum['partial failure'];
case 'failed':
- return RuleExecutionStatus.failed;
+ return RuleExecutionStatusEnum.failed;
default:
assertUnreachable(outcome);
- return RuleExecutionStatus.failed;
+ return RuleExecutionStatusEnum.failed;
}
};
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_summary.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_summary.mock.ts
index 3224b84ba2cef..59482e759f902 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_summary.mock.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/execution_summary.mock.ts
@@ -5,13 +5,13 @@
* 2.0.
*/
-import { RuleExecutionStatus } from './execution_status';
+import { RuleExecutionStatusEnum } from './execution_status.gen';
import type { RuleExecutionSummary } from './execution_summary';
const getSummarySucceeded = (): RuleExecutionSummary => ({
last_execution: {
date: '2020-02-18T15:26:49.783Z',
- status: RuleExecutionStatus.succeeded,
+ status: RuleExecutionStatusEnum.succeeded,
status_order: 0,
message: 'succeeded',
metrics: {
@@ -25,7 +25,7 @@ const getSummarySucceeded = (): RuleExecutionSummary => ({
const getSummaryFailed = (): RuleExecutionSummary => ({
last_execution: {
date: '2020-02-18T15:15:58.806Z',
- status: RuleExecutionStatus.failed,
+ status: RuleExecutionStatusEnum.failed,
status_order: 30,
message:
'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.',
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/index.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/index.ts
index b486b9d80957a..b4f003cf48228 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/index.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/index.ts
@@ -7,8 +7,8 @@
export * from './execution_event';
export * from './execution_metrics';
-export * from './execution_result';
+export * from './execution_result.gen';
export * from './execution_settings';
-export * from './execution_status';
+export * from './execution_status.gen';
export * from './execution_summary';
export * from './log_level';
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/log_level.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/log_level.ts
index 9add3b46b3ef3..495589b3cd432 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/log_level.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/model/log_level.ts
@@ -8,7 +8,8 @@
import { enumeration } from '@kbn/securitysolution-io-ts-types';
import { enumFromString } from '../../../../utils/enum_from_string';
import { assertUnreachable } from '../../../../utility_types';
-import { RuleExecutionStatus } from './execution_status';
+import type { RuleExecutionStatus } from './execution_status.gen';
+import { RuleExecutionStatusEnum } from './execution_status.gen';
export enum LogLevel {
'trace' = 'trace',
@@ -67,13 +68,13 @@ export const logLevelFromString = enumFromString(LogLevel);
export const logLevelFromExecutionStatus = (status: RuleExecutionStatus): LogLevel => {
switch (status) {
- case RuleExecutionStatus['going to run']:
- case RuleExecutionStatus.running:
- case RuleExecutionStatus.succeeded:
+ case RuleExecutionStatusEnum['going to run']:
+ case RuleExecutionStatusEnum.running:
+ case RuleExecutionStatusEnum.succeeded:
return LogLevel.info;
- case RuleExecutionStatus['partial failure']:
+ case RuleExecutionStatusEnum['partial failure']:
return LogLevel.warn;
- case RuleExecutionStatus.failed:
+ case RuleExecutionStatusEnum.failed:
return LogLevel.error;
default:
assertUnreachable(status);
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_events/get_rule_execution_events_route.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_events/get_rule_execution_events_route.gen.ts
new file mode 100644
index 0000000000000..751e571aae3fd
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_events/get_rule_execution_events_route.gen.ts
@@ -0,0 +1,85 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { z } from 'zod';
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ */
+
+import { RuleExecutionStatus } from '../../model/execution_status.gen';
+import {
+ SortFieldOfRuleExecutionResult,
+ RuleExecutionResult,
+} from '../../model/execution_result.gen';
+import { SortOrder } from '../../../model/sorting.gen';
+
+export type GetRuleExecutionEventsRequestQuery = z.infer;
+export const GetRuleExecutionEventsRequestQuery = z.object({
+ /**
+ * Start date of the time range to query
+ */
+ start: z.string().datetime(),
+ /**
+ * End date of the time range to query
+ */
+ end: z.string().datetime(),
+ /**
+ * Query text to filter results by
+ */
+ query_text: z.string().optional().default(''),
+ /**
+ * Comma-separated list of rule execution statuses to filter results by
+ */
+ status_filters: z
+ .preprocess(
+ (value: unknown) =>
+ typeof value === 'string' ? (value === '' ? [] : value.split(',')) : value,
+ z.array(RuleExecutionStatus)
+ )
+ .optional()
+ .default([]),
+ /**
+ * Field to sort results by
+ */
+ sort_field: SortFieldOfRuleExecutionResult.optional().default('timestamp'),
+ /**
+ * Sort order to sort results by
+ */
+ sort_order: SortOrder.optional().default('desc'),
+ /**
+ * Page number to return
+ */
+ page: z.coerce.number().int().optional().default(1),
+ /**
+ * Number of results per page
+ */
+ per_page: z.coerce.number().int().optional().default(20),
+});
+export type GetRuleExecutionEventsRequestQueryInput = z.input<
+ typeof GetRuleExecutionEventsRequestQuery
+>;
+
+export type GetRuleExecutionEventsRequestParams = z.infer<
+ typeof GetRuleExecutionEventsRequestParams
+>;
+export const GetRuleExecutionEventsRequestParams = z.object({
+ /**
+ * Saved object ID of the rule to get execution results for
+ */
+ ruleId: z.string().min(1),
+});
+export type GetRuleExecutionEventsRequestParamsInput = z.input<
+ typeof GetRuleExecutionEventsRequestParams
+>;
+
+export type GetRuleExecutionEventsResponse = z.infer;
+export const GetRuleExecutionEventsResponse = z.object({
+ events: z.array(RuleExecutionResult).optional(),
+ total: z.number().int().optional(),
+});
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_events/get_rule_execution_events_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_events/get_rule_execution_events_route.schema.yaml
new file mode 100644
index 0000000000000..677213bae4f2e
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_events/get_rule_execution_events_route.schema.yaml
@@ -0,0 +1,92 @@
+openapi: 3.0.0
+info:
+ title: Get Rule Execution Events API endpoint
+ version: '1'
+paths:
+ /internal/detection_engine/rules/{ruleId}/execution/events:
+ put:
+ operationId: GetRuleExecutionEvents
+ x-codegen-enabled: true
+ summary: Returns execution events of a given rule (aggregated by execution UUID) from Event Log.
+ tags:
+ - Rule Execution Log API
+ parameters:
+ - name: ruleId
+ in: path
+ required: true
+ description: Saved object ID of the rule to get execution results for
+ schema:
+ type: string
+ minLength: 1
+ - name: start
+ in: query
+ required: true
+ description: Start date of the time range to query
+ schema:
+ type: string
+ format: date-time
+ - name: end
+ in: query
+ required: true
+ description: End date of the time range to query
+ schema:
+ type: string
+ format: date-time
+ - name: query_text
+ in: query
+ required: false
+ description: Query text to filter results by
+ schema:
+ type: string
+ default: ''
+ - name: status_filters
+ in: query
+ required: false
+ description: Comma-separated list of rule execution statuses to filter results by
+ schema:
+ type: array
+ items:
+ $ref: '../../model/execution_status.schema.yaml#/components/schemas/RuleExecutionStatus'
+ default: []
+ - name: sort_field
+ in: query
+ required: false
+ description: Field to sort results by
+ schema:
+ $ref: '../../model/execution_result.schema.yaml#/components/schemas/SortFieldOfRuleExecutionResult'
+ default: timestamp
+ - name: sort_order
+ in: query
+ required: false
+ description: Sort order to sort results by
+ schema:
+ $ref: '../../../model/sorting.schema.yaml#/components/schemas/SortOrder'
+ default: desc
+ - name: page
+ in: query
+ required: false
+ description: Page number to return
+ schema:
+ type: integer
+ default: 1
+ - name: per_page
+ in: query
+ required: false
+ description: Number of results per page
+ schema:
+ type: integer
+ default: 20
+ responses:
+ 200:
+ description: Indicates a successful call
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ events:
+ type: array
+ items:
+ $ref: '../../model/execution_result.schema.yaml#/components/schemas/RuleExecutionResult'
+ total:
+ type: integer
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.gen.ts
new file mode 100644
index 0000000000000..442c45f3e8dc9
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.gen.ts
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { z } from 'zod';
+
+/*
+ * NOTICE: Do not edit this file manually.
+ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
+ */
+
+import { RuleExecutionStatus } from '../../model/execution_status.gen';
+import {
+ SortFieldOfRuleExecutionResult,
+ RuleExecutionResult,
+} from '../../model/execution_result.gen';
+import { SortOrder } from '../../../model/sorting.gen';
+
+export type GetRuleExecutionResultsRequestQuery = z.infer<
+ typeof GetRuleExecutionResultsRequestQuery
+>;
+export const GetRuleExecutionResultsRequestQuery = z.object({
+ /**
+ * Start date of the time range to query
+ */
+ start: z.string().datetime(),
+ /**
+ * End date of the time range to query
+ */
+ end: z.string().datetime(),
+ /**
+ * Query text to filter results by
+ */
+ query_text: z.string().optional().default(''),
+ /**
+ * Comma-separated list of rule execution statuses to filter results by
+ */
+ status_filters: z
+ .preprocess(
+ (value: unknown) =>
+ typeof value === 'string' ? (value === '' ? [] : value.split(',')) : value,
+ z.array(RuleExecutionStatus)
+ )
+ .optional()
+ .default([]),
+ /**
+ * Field to sort results by
+ */
+ sort_field: SortFieldOfRuleExecutionResult.optional().default('timestamp'),
+ /**
+ * Sort order to sort results by
+ */
+ sort_order: SortOrder.optional().default('desc'),
+ /**
+ * Page number to return
+ */
+ page: z.coerce.number().int().optional().default(1),
+ /**
+ * Number of results per page
+ */
+ per_page: z.coerce.number().int().optional().default(20),
+});
+export type GetRuleExecutionResultsRequestQueryInput = z.input<
+ typeof GetRuleExecutionResultsRequestQuery
+>;
+
+export type GetRuleExecutionResultsRequestParams = z.infer<
+ typeof GetRuleExecutionResultsRequestParams
+>;
+export const GetRuleExecutionResultsRequestParams = z.object({
+ /**
+ * Saved object ID of the rule to get execution results for
+ */
+ ruleId: z.string().min(1),
+});
+export type GetRuleExecutionResultsRequestParamsInput = z.input<
+ typeof GetRuleExecutionResultsRequestParams
+>;
+
+export type GetRuleExecutionResultsResponse = z.infer;
+export const GetRuleExecutionResultsResponse = z.object({
+ events: z.array(RuleExecutionResult).optional(),
+ total: z.number().int().optional(),
+});
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.mock.ts
index 9584616343332..cfcf0e6fe441a 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.mock.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.mock.ts
@@ -6,7 +6,7 @@
*/
import { ruleExecutionResultMock } from '../../model/execution_result.mock';
-import type { GetRuleExecutionResultsResponse } from './get_rule_execution_results_route';
+import type { GetRuleExecutionResultsResponse } from './get_rule_execution_results_route.gen';
const getSomeResponse = (): GetRuleExecutionResultsResponse => {
const results = ruleExecutionResultMock.getSomeResults();
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.schema.yaml
new file mode 100644
index 0000000000000..8bba4b2811e31
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.schema.yaml
@@ -0,0 +1,92 @@
+openapi: 3.0.0
+info:
+ title: Get Rule Execution Results API endpoint
+ version: '1'
+paths:
+ /internal/detection_engine/rules/{ruleId}/execution/results:
+ put:
+ operationId: GetRuleExecutionResults
+ x-codegen-enabled: true
+ summary: Returns execution results of a given rule (aggregated by execution UUID) from Event Log.
+ tags:
+ - Rule Execution Log API
+ parameters:
+ - name: ruleId
+ in: path
+ required: true
+ description: Saved object ID of the rule to get execution results for
+ schema:
+ type: string
+ minLength: 1
+ - name: start
+ in: query
+ required: true
+ description: Start date of the time range to query
+ schema:
+ type: string
+ format: date-time
+ - name: end
+ in: query
+ required: true
+ description: End date of the time range to query
+ schema:
+ type: string
+ format: date-time
+ - name: query_text
+ in: query
+ required: false
+ description: Query text to filter results by
+ schema:
+ type: string
+ default: ''
+ - name: status_filters
+ in: query
+ required: false
+ description: Comma-separated list of rule execution statuses to filter results by
+ schema:
+ type: array
+ items:
+ $ref: '../../model/execution_status.schema.yaml#/components/schemas/RuleExecutionStatus'
+ default: []
+ - name: sort_field
+ in: query
+ required: false
+ description: Field to sort results by
+ schema:
+ $ref: '../../model/execution_result.schema.yaml#/components/schemas/SortFieldOfRuleExecutionResult'
+ default: timestamp
+ - name: sort_order
+ in: query
+ required: false
+ description: Sort order to sort results by
+ schema:
+ $ref: '../../../model/sorting.schema.yaml#/components/schemas/SortOrder'
+ default: desc
+ - name: page
+ in: query
+ required: false
+ description: Page number to return
+ schema:
+ type: integer
+ default: 1
+ - name: per_page
+ in: query
+ required: false
+ description: Number of results per page
+ schema:
+ type: integer
+ default: 20
+ responses:
+ 200:
+ description: Indicates a successful call
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ events:
+ type: array
+ items:
+ $ref: '../../model/execution_result.schema.yaml#/components/schemas/RuleExecutionResult'
+ total:
+ type: integer
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.test.ts
index 53ed03003487e..9dc071b7ca1ea 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.test.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.test.ts
@@ -5,32 +5,29 @@
* 2.0.
*/
-import { pipe } from 'fp-ts/lib/pipeable';
-import { left } from 'fp-ts/lib/Either';
-import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';
+import { stringifyZodError } from '@kbn/securitysolution-es-utils';
+import { expectParseError, expectParseSuccess } from '../../../../../test/zod_helpers';
+import { RuleExecutionStatus } from '../../model';
+import { GetRuleExecutionResultsRequestQuery } from './get_rule_execution_results_route.gen';
-import { RULE_EXECUTION_STATUSES } from '../../model/execution_status';
-import {
- DefaultSortField,
- DefaultRuleExecutionStatusCsvArray,
-} from './get_rule_execution_results_route';
+const StatusFiltersSchema = GetRuleExecutionResultsRequestQuery.shape.status_filters;
+const SortFieldSchema = GetRuleExecutionResultsRequestQuery.shape.sort_field;
describe('Request schema of Get rule execution results', () => {
describe('DefaultRuleExecutionStatusCsvArray', () => {
describe('Validation succeeds', () => {
describe('when input is a single rule execution status', () => {
- const cases = RULE_EXECUTION_STATUSES.map((supportedStatus) => {
+ const cases = RuleExecutionStatus.options.map((supportedStatus) => {
return { input: supportedStatus };
});
cases.forEach(({ input }) => {
it(`${input}`, () => {
- const decoded = DefaultRuleExecutionStatusCsvArray.decode(input);
- const message = pipe(decoded, foldLeftRight);
const expectedOutput = [input]; // note that it's an array after decode
+ const result = StatusFiltersSchema.safeParse(input);
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(expectedOutput);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(expectedOutput);
});
});
});
@@ -43,12 +40,11 @@ describe('Request schema of Get rule execution results', () => {
cases.forEach(({ input }) => {
it(`${input}`, () => {
- const decoded = DefaultRuleExecutionStatusCsvArray.decode(input);
- const message = pipe(decoded, foldLeftRight);
const expectedOutput = input;
+ const result = StatusFiltersSchema.safeParse(input);
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(expectedOutput);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(expectedOutput);
});
});
});
@@ -67,11 +63,10 @@ describe('Request schema of Get rule execution results', () => {
cases.forEach(({ input, expectedOutput }) => {
it(`${input}`, () => {
- const decoded = DefaultRuleExecutionStatusCsvArray.decode(input);
- const message = pipe(decoded, foldLeftRight);
+ const result = StatusFiltersSchema.safeParse(input);
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(expectedOutput);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(expectedOutput);
});
});
});
@@ -82,37 +77,30 @@ describe('Request schema of Get rule execution results', () => {
const cases = [
{
input: 'val',
- expectedErrors: [
- 'Invalid value "val" supplied to "DefaultCsvArray"',
- ],
+ expectedErrors:
+ "0: Invalid enum value. Expected 'going to run' | 'running' | 'partial failure' | 'failed' | 'succeeded', received 'val'",
},
{
input: '5',
- expectedErrors: [
- 'Invalid value "5" supplied to "DefaultCsvArray"',
- ],
+ expectedErrors:
+ "0: Invalid enum value. Expected 'going to run' | 'running' | 'partial failure' | 'failed' | 'succeeded', received '5'",
},
{
input: 5,
- expectedErrors: [
- 'Invalid value "5" supplied to "DefaultCsvArray"',
- ],
+ expectedErrors: 'Expected array, received number',
},
{
input: {},
- expectedErrors: [
- 'Invalid value "{}" supplied to "DefaultCsvArray"',
- ],
+ expectedErrors: 'Expected array, received object',
},
];
cases.forEach(({ input, expectedErrors }) => {
it(`${input}`, () => {
- const decoded = DefaultRuleExecutionStatusCsvArray.decode(input);
- const message = pipe(decoded, foldLeftRight);
+ const result = StatusFiltersSchema.safeParse(input);
- expect(getPaths(left(message.errors))).toEqual(expectedErrors);
- expect(message.schema).toEqual({});
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(expectedErrors);
});
});
});
@@ -121,34 +109,27 @@ describe('Request schema of Get rule execution results', () => {
const cases = [
{
input: ['value 1', 5],
- expectedErrors: [
- 'Invalid value "value 1" supplied to "DefaultCsvArray"',
- 'Invalid value "5" supplied to "DefaultCsvArray"',
- ],
+ expectedErrors:
+ "0: Invalid enum value. Expected 'going to run' | 'running' | 'partial failure' | 'failed' | 'succeeded', received 'value 1', 1: Expected 'going to run' | 'running' | 'partial failure' | 'failed' | 'succeeded', received number",
},
{
input: ['value 1', 'succeeded'],
- expectedErrors: [
- 'Invalid value "value 1" supplied to "DefaultCsvArray"',
- ],
+ expectedErrors:
+ "0: Invalid enum value. Expected 'going to run' | 'running' | 'partial failure' | 'failed' | 'succeeded', received 'value 1'",
},
{
input: ['', 5, {}],
- expectedErrors: [
- 'Invalid value "" supplied to "DefaultCsvArray"',
- 'Invalid value "5" supplied to "DefaultCsvArray"',
- 'Invalid value "{}" supplied to "DefaultCsvArray"',
- ],
+ expectedErrors:
+ "0: Invalid enum value. Expected 'going to run' | 'running' | 'partial failure' | 'failed' | 'succeeded', received '', 1: Expected 'going to run' | 'running' | 'partial failure' | 'failed' | 'succeeded', received number, 2: Expected 'going to run' | 'running' | 'partial failure' | 'failed' | 'succeeded', received object",
},
];
cases.forEach(({ input, expectedErrors }) => {
it(`${input}`, () => {
- const decoded = DefaultRuleExecutionStatusCsvArray.decode(input);
- const message = pipe(decoded, foldLeftRight);
+ const result = StatusFiltersSchema.safeParse(input);
- expect(getPaths(left(message.errors))).toEqual(expectedErrors);
- expect(message.schema).toEqual({});
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(expectedErrors);
});
});
});
@@ -157,34 +138,27 @@ describe('Request schema of Get rule execution results', () => {
const cases = [
{
input: 'value 1,5',
- expectedErrors: [
- 'Invalid value "value 1" supplied to "DefaultCsvArray"',
- 'Invalid value "5" supplied to "DefaultCsvArray"',
- ],
+ expectedErrors:
+ "0: Invalid enum value. Expected 'going to run' | 'running' | 'partial failure' | 'failed' | 'succeeded', received 'value 1', 1: Invalid enum value. Expected 'going to run' | 'running' | 'partial failure' | 'failed' | 'succeeded', received '5'",
},
{
input: 'value 1,succeeded',
- expectedErrors: [
- 'Invalid value "value 1" supplied to "DefaultCsvArray"',
- ],
+ expectedErrors:
+ "0: Invalid enum value. Expected 'going to run' | 'running' | 'partial failure' | 'failed' | 'succeeded', received 'value 1'",
},
{
input: ',5,{}',
- expectedErrors: [
- 'Invalid value "" supplied to "DefaultCsvArray"',
- 'Invalid value "5" supplied to "DefaultCsvArray"',
- 'Invalid value "{}" supplied to "DefaultCsvArray"',
- ],
+ expectedErrors:
+ "0: Invalid enum value. Expected 'going to run' | 'running' | 'partial failure' | 'failed' | 'succeeded', received '', 1: Invalid enum value. Expected 'going to run' | 'running' | 'partial failure' | 'failed' | 'succeeded', received '5', 2: Invalid enum value. Expected 'going to run' | 'running' | 'partial failure' | 'failed' | 'succeeded', received '{}'",
},
];
cases.forEach(({ input, expectedErrors }) => {
it(`${input}`, () => {
- const decoded = DefaultRuleExecutionStatusCsvArray.decode(input);
- const message = pipe(decoded, foldLeftRight);
+ const result = StatusFiltersSchema.safeParse(input);
- expect(getPaths(left(message.errors))).toEqual(expectedErrors);
- expect(message.schema).toEqual({});
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(expectedErrors);
});
});
});
@@ -192,16 +166,14 @@ describe('Request schema of Get rule execution results', () => {
describe('Validation returns default value (an empty array)', () => {
describe('when input is', () => {
- const cases = [{ input: null }, { input: undefined }, { input: '' }, { input: [] }];
+ const cases = [{ input: undefined }, { input: '' }, { input: [] }];
cases.forEach(({ input }) => {
it(`${input}`, () => {
- const decoded = DefaultRuleExecutionStatusCsvArray.decode(input);
- const message = pipe(decoded, foldLeftRight);
- const expectedOutput: string[] = [];
+ const result = StatusFiltersSchema.safeParse(input);
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(expectedOutput);
+ expectParseSuccess(result);
+ expect(result.data).toEqual([]);
});
});
});
@@ -222,11 +194,9 @@ describe('Request schema of Get rule execution results', () => {
cases.forEach(({ input }) => {
it(`${input}`, () => {
- const decoded = DefaultSortField.decode(input);
- const message = pipe(decoded, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual(input);
+ const result = SortFieldSchema.safeParse(input);
+ expectParseSuccess(result);
+ expect(result.data).toEqual(input);
});
});
});
@@ -244,12 +214,10 @@ describe('Request schema of Get rule execution results', () => {
cases.forEach(({ input }) => {
it(`${input}`, () => {
- const decoded = DefaultSortField.decode(input);
- const message = pipe(decoded, foldLeftRight);
- const expectedErrors = [`Invalid value "${input}" supplied to "DefaultSortField"`];
-
- expect(getPaths(left(message.errors))).toEqual(expectedErrors);
- expect(message.schema).toEqual({});
+ const expectedErrors = `Invalid enum value. Expected 'timestamp' | 'duration_ms' | 'gap_duration_s' | 'indexing_duration_ms' | 'search_duration_ms' | 'schedule_delay_ms', received '${input}'`;
+ const result = SortFieldSchema.safeParse(input);
+ expectParseError(result);
+ expect(stringifyZodError(result.error)).toEqual(expectedErrors);
});
});
});
@@ -257,18 +225,38 @@ describe('Request schema of Get rule execution results', () => {
describe('Validation returns the default sort field "timestamp"', () => {
describe('when input is', () => {
- const cases = [{ input: null }, { input: undefined }];
+ const cases = [{ input: undefined }];
cases.forEach(({ input }) => {
it(`${input}`, () => {
- const decoded = DefaultSortField.decode(input);
- const message = pipe(decoded, foldLeftRight);
-
- expect(getPaths(left(message.errors))).toEqual([]);
- expect(message.schema).toEqual('timestamp');
+ const result = SortFieldSchema.safeParse(input);
+ expectParseSuccess(result);
+ expect(result.data).toEqual('timestamp');
});
});
});
});
});
+
+ describe('GetRuleExecutionResultsRequestQuery', () => {
+ it('should convert string values to numbers', () => {
+ const result = GetRuleExecutionResultsRequestQuery.safeParse({
+ start: '2021-08-01T00:00:00.000Z',
+ end: '2021-08-02T00:00:00.000Z',
+ page: '1',
+ per_page: '10',
+ });
+ expectParseSuccess(result);
+ expect(result.data).toEqual({
+ end: '2021-08-02T00:00:00.000Z',
+ page: 1,
+ per_page: 10,
+ query_text: '',
+ sort_field: 'timestamp',
+ sort_order: 'desc',
+ start: '2021-08-01T00:00:00.000Z',
+ status_filters: [],
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.ts
deleted file mode 100644
index 56370acbce5bd..0000000000000
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.ts
+++ /dev/null
@@ -1,86 +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 * as t from 'io-ts';
-
-import { DefaultPage, DefaultPerPage } from '@kbn/securitysolution-io-ts-alerting-types';
-import {
- defaultCsvArray,
- DefaultEmptyString,
- defaultValue,
- IsoDateString,
- NonEmptyString,
-} from '@kbn/securitysolution-io-ts-types';
-
-import { DefaultSortOrderDesc } from '../../../model';
-import {
- RuleExecutionResult,
- SortFieldOfRuleExecutionResult,
- TRuleExecutionStatus,
-} from '../../model';
-
-/**
- * Types the DefaultRuleExecutionStatusCsvArray as:
- * - If not specified, then a default empty array will be set
- * - If an array is sent in, then the array will be validated to ensure all elements are a RuleExecutionStatus
- * (or that the array is empty)
- * - If a CSV string is sent in, then it will be parsed to an array which will be validated
- */
-export const DefaultRuleExecutionStatusCsvArray = defaultCsvArray(TRuleExecutionStatus);
-
-/**
- * Types the DefaultSortField as:
- * - If undefined, then a default sort field of 'timestamp' will be set
- * - If a string is sent in, then the string will be validated to ensure it is as valid sortFields
- */
-export const DefaultSortField = defaultValue(
- SortFieldOfRuleExecutionResult,
- 'timestamp',
- 'DefaultSortField'
-);
-
-/**
- * Path parameters of the API route.
- */
-export type GetRuleExecutionResultsRequestParams = t.TypeOf<
- typeof GetRuleExecutionResultsRequestParams
->;
-export const GetRuleExecutionResultsRequestParams = t.exact(
- t.type({
- ruleId: NonEmptyString,
- })
-);
-
-/**
- * Query string parameters of the API route.
- */
-export type GetRuleExecutionResultsRequestQuery = t.TypeOf<
- typeof GetRuleExecutionResultsRequestQuery
->;
-export const GetRuleExecutionResultsRequestQuery = t.exact(
- t.type({
- start: IsoDateString,
- end: IsoDateString,
- query_text: DefaultEmptyString, // defaults to ''
- status_filters: DefaultRuleExecutionStatusCsvArray, // defaults to []
- sort_field: DefaultSortField, // defaults to 'timestamp'
- sort_order: DefaultSortOrderDesc, // defaults to 'desc'
- page: DefaultPage, // defaults to 1
- per_page: DefaultPerPage, // defaults to 20
- })
-);
-
-/**
- * Response body of the API route.
- */
-export type GetRuleExecutionResultsResponse = t.TypeOf;
-export const GetRuleExecutionResultsResponse = t.exact(
- t.type({
- events: t.array(RuleExecutionResult),
- total: t.number,
- })
-);
diff --git a/x-pack/plugins/security_solution/common/api/timeline/model/api.ts b/x-pack/plugins/security_solution/common/api/timeline/model/api.ts
index 24a9a24f8fd8e..c164068f23709 100644
--- a/x-pack/plugins/security_solution/common/api/timeline/model/api.ts
+++ b/x-pack/plugins/security_solution/common/api/timeline/model/api.ts
@@ -19,7 +19,7 @@ import {
SavedObjectResolveAliasTargetId,
SavedObjectResolveOutcome,
} from '../../detection_engine/model/rule_schema';
-import { errorSchema, success, success_count as successCount } from '../../detection_engine';
+import { ErrorSchema, success, success_count as successCount } from '../../detection_engine';
export const BareNoteSchema = runtimeTypes.intersection([
runtimeTypes.type({
@@ -499,7 +499,7 @@ export const importTimelineResultSchema = runtimeTypes.exact(
success_count: successCount,
timelines_installed: PositiveInteger,
timelines_updated: PositiveInteger,
- errors: runtimeTypes.array(errorSchema),
+ errors: runtimeTypes.array(ErrorSchema),
})
);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts
index b8fe93efc722a..bfc31df88ec97 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/rule_filtering.ts
@@ -6,7 +6,8 @@
*/
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
-import { RuleExecutionStatus } from '../../api/detection_engine';
+import type { RuleExecutionStatus } from '../../api/detection_engine';
+import { RuleExecutionStatusEnum } from '../../api/detection_engine';
import { prepareKQLStringParam } from '../../utils/kql';
import {
ENABLED_FIELD,
@@ -75,11 +76,11 @@ export function convertRulesFilterToKQL({
kql.push(`NOT ${convertRuleTypesToKQL(excludeRuleTypes)}`);
}
- if (ruleExecutionStatus === RuleExecutionStatus.succeeded) {
+ if (ruleExecutionStatus === RuleExecutionStatusEnum.succeeded) {
kql.push(`${LAST_RUN_OUTCOME_FIELD}: "succeeded"`);
- } else if (ruleExecutionStatus === RuleExecutionStatus['partial failure']) {
+ } else if (ruleExecutionStatus === RuleExecutionStatusEnum['partial failure']) {
kql.push(`${LAST_RUN_OUTCOME_FIELD}: "warning"`);
- } else if (ruleExecutionStatus === RuleExecutionStatus.failed) {
+ } else if (ruleExecutionStatus === RuleExecutionStatusEnum.failed) {
kql.push(`${LAST_RUN_OUTCOME_FIELD}: "failed"`);
}
diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts
index 8c3c697b80e2c..a3b3ebaf26142 100644
--- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts
@@ -11,8 +11,8 @@ import { createFleetAuthzMock } from '@kbn/fleet-plugin/common/mocks';
import { createLicenseServiceMock } from '../../../license/mocks';
import type { EndpointAuthzKeyList } from '../../types/authz';
import {
- RESPONSE_CONSOLE_ACTION_COMMANDS_TO_RBAC_FEATURE_CONTROL,
CONSOLE_RESPONSE_ACTION_COMMANDS,
+ RESPONSE_CONSOLE_ACTION_COMMANDS_TO_RBAC_FEATURE_CONTROL,
type ResponseConsoleRbacControls,
} from '../response_actions/constants';
@@ -150,6 +150,14 @@ describe('Endpoint Authz service', () => {
expect(authz[auth]).toBe(true);
});
+ it.each<[EndpointAuthzKeyList[number], string]>([
+ ['canReadEndpointExceptions', 'showEndpointExceptions'],
+ ['canWriteEndpointExceptions', 'crudEndpointExceptions'],
+ ])('%s should be true if `endpointExceptionsPrivileges.%s` is `true`', (auth) => {
+ const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles);
+ expect(authz[auth]).toBe(true);
+ });
+
it.each<[EndpointAuthzKeyList[number], string[]]>([
['canWriteEndpointList', ['writeEndpointList']],
['canReadEndpointList', ['readEndpointList']],
@@ -181,6 +189,20 @@ describe('Endpoint Authz service', () => {
privileges.forEach((privilege) => {
fleetAuthz.packagePrivileges!.endpoint.actions[privilege].executePackageAction = false;
});
+
+ const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles);
+ expect(authz[auth]).toBe(false);
+ });
+
+ it.each<[EndpointAuthzKeyList[number], string[]]>([
+ ['canReadEndpointExceptions', ['showEndpointExceptions']],
+ ['canWriteEndpointExceptions', ['crudEndpointExceptions']],
+ ])('%s should be false if `endpointExceptionsPrivileges.%s` is `false`', (auth, privileges) => {
+ privileges.forEach((privilege) => {
+ // @ts-ignore
+ fleetAuthz.endpointExceptionsPrivileges!.actions[privilege] = false;
+ });
+
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles);
expect(authz[auth]).toBe(false);
});
@@ -281,6 +303,8 @@ describe('Endpoint Authz service', () => {
canReadBlocklist: false,
canWriteEventFilters: false,
canReadEventFilters: false,
+ canReadEndpointExceptions: false,
+ canWriteEndpointExceptions: false,
});
});
});
diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts
index c0c5ee06d5488..922e8c3cdd383 100644
--- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts
@@ -30,6 +30,13 @@ export function hasKibanaPrivilege(
return fleetAuthz.packagePrivileges?.endpoint?.actions[privilege].executePackageAction ?? false;
}
+export function hasEndpointExceptionsPrivilege(
+ fleetAuthz: FleetAuthz,
+ privilege: 'showEndpointExceptions' | 'crudEndpointExceptions'
+): boolean {
+ return fleetAuthz.endpointExceptionsPrivileges?.actions[privilege] ?? false;
+}
+
/**
* Used by both the server and the UI to generate the Authorization for access to Endpoint related
* functionality
@@ -84,6 +91,15 @@ export const calculateEndpointAuthz = (
const canWriteExecuteOperations = hasKibanaPrivilege(fleetAuthz, 'writeExecuteOperations');
+ const canReadEndpointExceptions = hasEndpointExceptionsPrivilege(
+ fleetAuthz,
+ 'showEndpointExceptions'
+ );
+ const canWriteEndpointExceptions = hasEndpointExceptionsPrivilege(
+ fleetAuthz,
+ 'crudEndpointExceptions'
+ );
+
const authz: EndpointAuthz = {
canWriteSecuritySolution,
canReadSecuritySolution,
@@ -123,6 +139,8 @@ export const calculateEndpointAuthz = (
canReadBlocklist,
canWriteEventFilters,
canReadEventFilters,
+ canReadEndpointExceptions,
+ canWriteEndpointExceptions,
};
// Response console is only accessible when license is Enterprise and user has access to any
@@ -172,5 +190,7 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => {
canReadBlocklist: false,
canWriteEventFilters: false,
canReadEventFilters: false,
+ canReadEndpointExceptions: false,
+ canWriteEndpointExceptions: false,
};
};
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/authz.ts b/x-pack/plugins/security_solution/common/endpoint/types/authz.ts
index 23ed5cbe7c439..1ec8f84a87073 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/authz.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/authz.ts
@@ -10,74 +10,79 @@
* used both on the client and server for consistency
*/
export interface EndpointAuthz {
- /** if user has write permissions to the security solution app */
+ /** If the user has write permissions to the security solution app */
canWriteSecuritySolution: boolean;
- /** if user has read permissions to the security solution app */
+ /** If the user has read permissions to the security solution app */
canReadSecuritySolution: boolean;
- /** If user has permissions to access Fleet */
+ /** If the user has permissions to access Fleet */
canAccessFleet: boolean;
- /** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */
+ /** If the user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */
canAccessEndpointManagement: boolean;
- /** If user has permissions to access Actions Log management and also has a platinum license (used for endpoint details flyout) */
+ /** If the user has permissions to access Actions Log management and also has a platinum license (used for endpoint details flyout) */
canAccessEndpointActionsLogManagement: boolean;
- /** if user has permissions to create Artifacts by Policy */
+ /** If the user has permissions to create Artifacts by Policy */
canCreateArtifactsByPolicy: boolean;
- /** if user has write permissions to endpoint list */
+ /** If the user has write permissions to endpoint list */
canWriteEndpointList: boolean;
- /** if user has read permissions to endpoint list */
+ /** If the user has read permissions to endpoint list */
canReadEndpointList: boolean;
- /** if user has write permissions for policy management */
+ /** If the user has write permissions for policy management */
canWritePolicyManagement: boolean;
- /** if user has read permissions for policy management */
+ /** If the user has read permissions for policy management */
canReadPolicyManagement: boolean;
- /** if user has write permissions for actions log management */
+ /** If the user has write permissions for actions log management */
canWriteActionsLogManagement: boolean;
- /** if user has read permissions for actions log management */
+ /** If the user has read permissions for actions log management */
canReadActionsLogManagement: boolean;
- /** If user has permissions to isolate hosts */
+ /** If the user has permissions to isolate hosts */
canIsolateHost: boolean;
- /** If user has permissions to un-isolate (release) hosts */
+ /** If the user has permissions to un-isolate (release) hosts */
canUnIsolateHost: boolean;
- /** If user has permissions to kill process on hosts */
+ /** If the user has permissions to kill process on hosts */
canKillProcess: boolean;
- /** If user has permissions to suspend process on hosts */
+ /** If the user has permissions to suspend process on hosts */
canSuspendProcess: boolean;
- /** If user has permissions to get running processes on hosts */
+ /** If the user has permissions to get running processes on hosts */
canGetRunningProcesses: boolean;
- /** If user has permissions to use the Response Actions Console */
+ /** If the user has permissions to use the Response Actions Console */
canAccessResponseConsole: boolean;
- /** If user has write permissions to use execute action */
+ /** If the user has write permissions to use execute action */
canWriteExecuteOperations: boolean;
- /** If user has write permissions to use file operations */
+ /** If the user has write permissions to use file operations */
canWriteFileOperations: boolean;
- /** if user has write permissions for trusted applications */
+ /** If the user has write permissions for trusted applications */
canWriteTrustedApplications: boolean;
- /** if user has read permissions for trusted applications */
+ /** If the user has read permissions for trusted applications */
canReadTrustedApplications: boolean;
- /** if user has write permissions for host isolation exceptions */
+ /** If the user has write permissions for host isolation exceptions */
canWriteHostIsolationExceptions: boolean;
- /** if user has read permissions for host isolation exceptions */
+ /** If the user has read permissions for host isolation exceptions */
canReadHostIsolationExceptions: boolean;
/**
- * if user has permissions to access host isolation exceptions. This could be set to false, while
+ * If the user has permissions to access host isolation exceptions. This could be set to false, while
* `canReadHostIsolationExceptions` is true in cases where the license might have been downgraded.
* It is used to show the UI elements that allow users to navigate to the host isolation exceptions.
*/
canAccessHostIsolationExceptions: boolean;
/**
- * if user has permissions to delete host isolation exceptions. This could be set to true, while
+ * If the user has permissions to delete host isolation exceptions. This could be set to true, while
* `canWriteHostIsolationExceptions` is false in cases where the license might have been downgraded.
* In that use case, users should still be allowed to ONLY delete entries.
*/
canDeleteHostIsolationExceptions: boolean;
- /** if user has write permissions for blocklist entries */
+ /** If the user has write permissions for blocklist entries */
canWriteBlocklist: boolean;
- /** if user has read permissions for blocklist entries */
+ /** If the user has read permissions for blocklist entries */
canReadBlocklist: boolean;
- /** if user has write permissions for event filters */
+ /** If the user has write permissions for event filters */
canWriteEventFilters: boolean;
- /** if user has read permissions for event filters */
+ /** If the user has read permissions for event filters */
canReadEventFilters: boolean;
+
+ /** if the user has write permissions for endpoint exceptions */
+ canReadEndpointExceptions: boolean;
+ /** if the user has read permissions for endpoint exceptions */
+ canWriteEndpointExceptions: boolean;
}
export type EndpointAuthzKeyList = Array;
diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx
index 4b9941b1cbe5d..fcf4fd666a58a 100644
--- a/x-pack/plugins/security_solution/public/app/app.tsx
+++ b/x-pack/plugins/security_solution/public/app/app.tsx
@@ -38,7 +38,6 @@ interface StartAppComponent {
children: React.ReactNode;
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
- setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
store: Store;
theme$: AppMountParameters['theme$'];
}
@@ -46,7 +45,6 @@ interface StartAppComponent {
const StartAppComponent: FC = ({
children,
history,
- setHeaderActionMenu,
onAppLeave,
store,
theme$,
@@ -79,11 +77,7 @@ const StartAppComponent: FC = ({
>
-
+
{children}
@@ -113,7 +107,6 @@ interface SecurityAppComponentProps {
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
services: StartServices;
- setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
store: Store;
theme$: AppMountParameters['theme$'];
}
@@ -123,7 +116,6 @@ const SecurityAppComponent: React.FC = ({
history,
onAppLeave,
services,
- setHeaderActionMenu,
store,
theme$,
}) => {
@@ -137,13 +129,7 @@ const SecurityAppComponent: React.FC = ({
}}
>
-
+
{children}
diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx
index fe0b5bd500dc8..bfce21c47867c 100644
--- a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx
@@ -49,7 +49,6 @@ jest.mock('react-reverse-portal', () => ({
}));
describe('global header', () => {
- const mockSetHeaderActionMenu = jest.fn();
const state = {
...mockGlobalState,
timeline: {
@@ -75,7 +74,7 @@ describe('global header', () => {
]);
const { getByText } = render(
-
+
);
expect(getByText('Add integrations')).toBeInTheDocument();
@@ -87,7 +86,7 @@ describe('global header', () => {
]);
const { queryByTestId } = render(
-
+
);
const link = queryByTestId('add-data');
@@ -98,7 +97,7 @@ describe('global header', () => {
(useLocation as jest.Mock).mockReturnValue({ pathname: THREAT_INTELLIGENCE_PATH });
const { queryByTestId } = render(
-
+
);
const link = queryByTestId('add-data');
@@ -118,7 +117,7 @@ describe('global header', () => {
);
const { queryByTestId } = render(
-
+
);
const link = queryByTestId('add-data');
@@ -130,7 +129,7 @@ describe('global header', () => {
const { getByTestId } = render(
-
+
);
expect(getByTestId('sourcerer-trigger')).toBeInTheDocument();
@@ -141,7 +140,7 @@ describe('global header', () => {
const { getByTestId } = render(
-
+
);
expect(getByTestId('sourcerer-trigger')).toBeInTheDocument();
@@ -166,7 +165,7 @@ describe('global header', () => {
const { queryByTestId } = render(
-
+
);
@@ -180,7 +179,7 @@ describe('global header', () => {
const { findByTestId } = render(
-
+
);
diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
index bde0b71a43270..e5a12721a6292 100644
--- a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
@@ -15,11 +15,10 @@ import { useLocation } from 'react-router-dom';
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
import { i18n } from '@kbn/i18n';
-import type { AppMountParameters } from '@kbn/core/public';
-import { toMountPoint } from '@kbn/kibana-react-plugin/public';
+import { toMountPoint } from '@kbn/react-kibana-mount';
import { MlPopover } from '../../../common/components/ml_popover/ml_popover';
import { useKibana } from '../../../common/lib/kibana';
-import { isDetectionsPath } from '../../../helpers';
+import { isDetectionsPath, isDashboardViewPath } from '../../../helpers';
import { Sourcerer } from '../../../common/components/sourcerer';
import { TimelineId } from '../../../../common/types/timeline';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
@@ -37,63 +36,69 @@ const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.globalHeader.butt
* This component uses the reverse portal to add the Add Data, ML job settings, and AI Assistant buttons on the
* right hand side of the Kibana global header
*/
-export const GlobalHeader = React.memo(
- ({ setHeaderActionMenu }: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'] }) => {
- const portalNode = useMemo(() => createHtmlPortalNode(), []);
- const { theme } = useKibana().services;
- const { pathname } = useLocation();
+export const GlobalHeader = React.memo(() => {
+ const portalNode = useMemo(() => createHtmlPortalNode(), []);
+ const { theme, setHeaderActionMenu, i18n: kibanaServiceI18n } = useKibana().services;
+ const { pathname } = useLocation();
- const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
- const showTimeline = useShallowEqualSelector(
- (state) => (getTimeline(state, TimelineId.active) ?? timelineDefaults).show
- );
+ const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
+ const showTimeline = useShallowEqualSelector(
+ (state) => (getTimeline(state, TimelineId.active) ?? timelineDefaults).show
+ );
- const sourcererScope = getScopeFromPath(pathname);
- const showSourcerer = showSourcererByPath(pathname);
+ const sourcererScope = getScopeFromPath(pathname);
+ const showSourcerer = showSourcererByPath(pathname);
+ const dashboardViewPath = isDashboardViewPath(pathname);
- const { href, onClick } = useAddIntegrationsUrl();
+ const { href, onClick } = useAddIntegrationsUrl();
- useEffect(() => {
- setHeaderActionMenu((element) => {
- const mount = toMountPoint(, { theme$: theme.theme$ });
- return mount(element);
+ useEffect(() => {
+ setHeaderActionMenu((element) => {
+ const mount = toMountPoint(, {
+ theme,
+ i18n: kibanaServiceI18n,
});
+ return mount(element);
+ });
- return () => {
- portalNode.unmount();
- setHeaderActionMenu(undefined);
- };
- }, [portalNode, setHeaderActionMenu, theme.theme$]);
-
- return (
-
-
- {isDetectionsPath(pathname) && (
-
-
-
- )}
+ return () => {
+ /* Dashboard mounts an edit toolbar, it should be restored when leaving dashboard editing page */
+ if (dashboardViewPath) {
+ return;
+ }
+ portalNode.unmount();
+ setHeaderActionMenu(undefined);
+ };
+ }, [portalNode, setHeaderActionMenu, theme, kibanaServiceI18n, dashboardViewPath]);
+ return (
+
+
+ {isDetectionsPath(pathname) && (
-
-
- {BUTTON_ADD_DATA}
-
- {showSourcerer && !showTimeline && (
-
- )}
-
-
+
-
-
- );
- }
-);
+ )}
+
+
+
+
+ {BUTTON_ADD_DATA}
+
+ {showSourcerer && !showTimeline && (
+
+ )}
+
+
+
+
+
+ );
+});
GlobalHeader.displayName = 'GlobalHeader';
diff --git a/x-pack/plugins/security_solution/public/app/home/index.test.tsx b/x-pack/plugins/security_solution/public/app/home/index.test.tsx
index 9bce539a0b311..fc62a8236f9cc 100644
--- a/x-pack/plugins/security_solution/public/app/home/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/index.test.tsx
@@ -106,6 +106,7 @@ jest.mock('../../timelines/store/timeline', () => ({
const mockedFilterManager = new FilterManager(coreMock.createStart().uiSettings);
const mockGetSavedQuery = jest.fn();
+const mockSetHeaderActionMenu = jest.fn();
const dummyFilter: Filter = {
meta: {
@@ -198,6 +199,7 @@ jest.mock('../../common/lib/kibana', () => {
savedQueries: { getSavedQuery: mockGetSavedQuery },
},
},
+ setHeaderActionMenu: mockSetHeaderActionMenu,
},
}),
KibanaServices: {
@@ -226,7 +228,7 @@ describe('HomePage', () => {
it('calls useInitializeUrlParam for appQuery, filters and savedQuery', () => {
render(
-
+
@@ -252,7 +254,7 @@ describe('HomePage', () => {
render(
-
+
@@ -294,7 +296,7 @@ describe('HomePage', () => {
render(
-
+
@@ -326,7 +328,7 @@ describe('HomePage', () => {
render(
-
+
@@ -361,7 +363,7 @@ describe('HomePage', () => {
render(
-
+
@@ -378,7 +380,7 @@ describe('HomePage', () => {
render(
-
+
@@ -420,7 +422,7 @@ describe('HomePage', () => {
render(
-
+
@@ -465,7 +467,7 @@ describe('HomePage', () => {
render(
-
+
@@ -515,7 +517,7 @@ describe('HomePage', () => {
const TestComponent = () => (
-
+
@@ -572,7 +574,7 @@ describe('HomePage', () => {
const TestComponent = () => (
-
+
@@ -612,7 +614,7 @@ describe('HomePage', () => {
render(
-
+
@@ -637,7 +639,7 @@ describe('HomePage', () => {
const TestComponent = () => (
-
+
@@ -669,7 +671,7 @@ describe('HomePage', () => {
const TestComponent = () => (
-
+
diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx
index b951501b16cb7..bded1d58c8d84 100644
--- a/x-pack/plugins/security_solution/public/app/home/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/index.tsx
@@ -8,7 +8,6 @@
import React from 'react';
import { useLocation } from 'react-router-dom';
-import type { AppMountParameters } from '@kbn/core/public';
import { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper';
import { SecuritySolutionAppWrapper } from '../../common/components/page';
@@ -33,10 +32,9 @@ import { AssistantOverlay } from '../../assistant/overlay';
interface HomePageProps {
children: React.ReactNode;
- setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
}
-const HomePageComponent: React.FC = ({ children, setHeaderActionMenu }) => {
+const HomePageComponent: React.FC = ({ children }) => {
const { pathname } = useLocation();
useInitSourcerer(getScopeFromPath(pathname));
useUrlState();
@@ -58,7 +56,7 @@ const HomePageComponent: React.FC = ({ children, setHeaderActionM
<>
-
+
{children}
diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx
index e635c2d9fd3d3..6f0fc3eb8d01c 100644
--- a/x-pack/plugins/security_solution/public/app/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/index.tsx
@@ -16,7 +16,6 @@ export const renderApp = ({
element,
history,
onAppLeave,
- setHeaderActionMenu,
services,
store,
usageCollection,
@@ -31,7 +30,6 @@ export const renderApp = ({
history={history}
onAppLeave={onAppLeave}
services={services}
- setHeaderActionMenu={setHeaderActionMenu}
store={store}
theme$={theme$}
>
diff --git a/x-pack/plugins/security_solution/public/app/routes.tsx b/x-pack/plugins/security_solution/public/app/routes.tsx
index af5aaba76363f..73fe2615b0e5a 100644
--- a/x-pack/plugins/security_solution/public/app/routes.tsx
+++ b/x-pack/plugins/security_solution/public/app/routes.tsx
@@ -10,7 +10,7 @@ import type { FC } from 'react';
import React, { memo, useEffect } from 'react';
import { Router, Routes, Route } from '@kbn/shared-ux-router';
import { useDispatch } from 'react-redux';
-import type { AppLeaveHandler, AppMountParameters } from '@kbn/core/public';
+import type { AppLeaveHandler } from '@kbn/core/public';
import { APP_ID } from '../../common/constants';
import { RouteCapture } from '../common/components/endpoint/route_capture';
@@ -24,15 +24,9 @@ interface RouterProps {
children: React.ReactNode;
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
- setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
}
-const PageRouterComponent: FC = ({
- children,
- history,
- onAppLeave,
- setHeaderActionMenu,
-}) => {
+const PageRouterComponent: FC = ({ children, history, onAppLeave }) => {
const { cases } = useKibana().services;
const CasesContext = cases.ui.getCasesContext();
const userCasesPermissions = useGetUserCasesPermissions();
@@ -55,7 +49,7 @@ const PageRouterComponent: FC = ({
- {children}
+ {children}
diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts
rename to x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.tsx
diff --git a/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts b/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts
index d7401ff19d916..9d13b857c155e 100644
--- a/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts
+++ b/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts
@@ -34,3 +34,14 @@ export const getTagsByName = jest
export const createTag = jest
.fn()
.mockImplementation(() => Promise.resolve(DEFAULT_CREATE_TAGS_RESPONSE[0]));
+
+export const fetchTags = jest.fn().mockImplementation(({ tagIds }: { tagIds: string[] }) =>
+ Promise.resolve(
+ tagIds.map((id, i) => ({
+ id,
+ name: `${MOCK_TAG_NAME}-${i}`,
+ description: 'test tag description',
+ color: '#2c7b8',
+ }))
+ )
+);
diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts
index 9c78db17abf37..7c9d7c5f0656c 100644
--- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts
@@ -121,6 +121,7 @@ export const createStartServicesMock = (
const cloudExperiments = cloudExperimentsMock.createStartMock();
const guidedOnboarding = guidedOnboardingMock.createStart();
const cloud = cloudMock.createStart();
+ const mockSetHeaderActionMenu = jest.fn();
return {
...core,
@@ -220,6 +221,7 @@ export const createStartServicesMock = (
customDataService,
uiActions: uiActionsPluginMock.createStartContract(),
savedSearch: savedSearchPluginMock.createStartContract(),
+ setHeaderActionMenu: mockSetHeaderActionMenu,
} as unknown as StartServices;
};
diff --git a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.test.tsx b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.test.tsx
index c01f07fa36653..9c6df7bb6e395 100644
--- a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.test.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.test.tsx
@@ -12,13 +12,10 @@ import { DashboardRenderer as DashboardContainerRenderer } from '@kbn/dashboard-
import { TestProviders } from '../../common/mock';
import { DashboardRenderer } from './dashboard_renderer';
-jest.mock('@kbn/dashboard-plugin/public', () => {
- const actual = jest.requireActual('@kbn/dashboard-plugin/public');
- return {
- ...actual,
- DashboardRenderer: jest.fn().mockReturnValue(),
- };
-});
+jest.mock('@kbn/dashboard-plugin/public', () => ({
+ DashboardRenderer: jest.fn().mockReturnValue(),
+ DashboardTopNav: jest.fn().mockReturnValue(),
+}));
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
diff --git a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.tsx b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.tsx
index aa51842a33c1e..73538439de568 100644
--- a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_renderer.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useEffect, useState } from 'react';
-import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
+import type { DashboardAPI, DashboardCreationOptions } from '@kbn/dashboard-plugin/public';
import { DashboardRenderer as DashboardContainerRenderer } from '@kbn/dashboard-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import type { Filter, Query } from '@kbn/es-query';
@@ -13,9 +13,14 @@ import type { Filter, Query } from '@kbn/es-query';
import { useDispatch } from 'react-redux';
import { InputsModelId } from '../../common/store/inputs/constants';
import { inputsActions } from '../../common/store/inputs';
+import { useKibana } from '../../common/lib/kibana';
+import { APP_UI_ID } from '../../../common';
+import { useSecurityTags } from '../context/dashboard_context';
+import { DASHBOARDS_PATH } from '../../../common/constants';
const DashboardRendererComponent = ({
canReadDashboard,
+ dashboardContainer,
filters,
id,
inputId = InputsModelId.global,
@@ -23,8 +28,10 @@ const DashboardRendererComponent = ({
query,
savedObjectId,
timeRange,
+ viewMode = ViewMode.VIEW,
}: {
canReadDashboard: boolean;
+ dashboardContainer?: DashboardAPI;
filters?: Filter[];
id: string;
inputId?: InputsModelId.global | InputsModelId.timeline;
@@ -37,17 +44,36 @@ const DashboardRendererComponent = ({
to: string;
toStr?: string | undefined;
};
+ viewMode?: ViewMode;
}) => {
+ const { embeddable } = useKibana().services;
const dispatch = useDispatch();
- const [dashboardContainer, setDashboardContainer] = useState();
- const getCreationOptions = useCallback(
+ const securityTags = useSecurityTags();
+ const firstSecurityTagId = securityTags?.[0]?.id;
+
+ const isCreateDashboard = !savedObjectId;
+
+ const getCreationOptions: () => Promise = useCallback(
() =>
Promise.resolve({
- getInitialInput: () => ({ timeRange, viewMode: ViewMode.VIEW, query, filters }),
+ useSessionStorageIntegration: true,
useControlGroupIntegration: true,
+ getInitialInput: () => ({
+ timeRange,
+ viewMode,
+ query,
+ filters,
+ }),
+ getIncomingEmbeddable: () =>
+ embeddable.getStateTransfer().getIncomingEmbeddablePackage(APP_UI_ID, true),
+ getEmbeddableAppContext: (dashboardId?: string) => ({
+ getCurrentPath: () =>
+ dashboardId ? `${DASHBOARDS_PATH}/${dashboardId}/edit` : `${DASHBOARDS_PATH}/create`,
+ currentAppId: APP_UI_ID,
+ }),
}),
- [filters, query, timeRange]
+ [embeddable, filters, query, timeRange, viewMode]
);
const refetchByForceRefresh = useCallback(() => {
@@ -73,20 +99,33 @@ const DashboardRendererComponent = ({
dashboardContainer?.updateInput({ timeRange, query, filters });
}, [dashboardContainer, filters, query, timeRange]);
- const handleDashboardLoaded = useCallback(
- (container: DashboardAPI) => {
- setDashboardContainer(container);
- onDashboardContainerLoaded?.(container);
- },
- [onDashboardContainerLoaded]
- );
- return savedObjectId && canReadDashboard ? (
-
- ) : null;
+ useEffect(() => {
+ if (isCreateDashboard && firstSecurityTagId)
+ dashboardContainer?.updateInput({ tags: [firstSecurityTagId] });
+ }, [dashboardContainer, firstSecurityTagId, isCreateDashboard]);
+
+ /** Dashboard renderer is stored in the state as it's a temporary solution for
+ * https://github.com/elastic/kibana/issues/167751
+ **/
+ const [dashboardContainerRenderer, setDashboardContainerRenderer] = useState<
+ React.ReactElement | undefined
+ >(undefined);
+
+ useEffect(() => {
+ setDashboardContainerRenderer(
+
+ );
+
+ return () => {
+ setDashboardContainerRenderer(undefined);
+ };
+ }, [getCreationOptions, onDashboardContainerLoaded, refetchByForceRefresh, savedObjectId]);
+
+ return canReadDashboard ? <>{dashboardContainerRenderer}> : null;
};
DashboardRendererComponent.displayName = 'DashboardRendererComponent';
export const DashboardRenderer = React.memo(DashboardRendererComponent);
diff --git a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_title.tsx b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_title.tsx
new file mode 100644
index 0000000000000..67d8e73bacdb6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_title.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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, { useEffect } from 'react';
+import { EuiLoadingSpinner } from '@elastic/eui';
+import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
+import { EDIT_DASHBOARD_TITLE } from '../pages/details/translations';
+
+const DashboardTitleComponent = ({
+ dashboardContainer,
+ onTitleLoaded,
+}: {
+ dashboardContainer: DashboardAPI;
+ onTitleLoaded: (title: string) => void;
+}) => {
+ const dashboardTitle = dashboardContainer.select((state) => state.explicitInput.title).trim();
+ const title =
+ dashboardTitle && dashboardTitle.length !== 0 ? dashboardTitle : EDIT_DASHBOARD_TITLE;
+
+ useEffect(() => {
+ onTitleLoaded(title);
+ }, [dashboardContainer, title, onTitleLoaded]);
+
+ return dashboardTitle != null ? {title} : ;
+};
+
+export const DashboardTitle = React.memo(DashboardTitleComponent);
diff --git a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.test.tsx b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.test.tsx
new file mode 100644
index 0000000000000..da0bf3e3dbcea
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.test.tsx
@@ -0,0 +1,105 @@
+/*
+ * 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 { render, screen } from '@testing-library/react';
+import { DashboardToolBar } from './dashboard_tool_bar';
+import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
+import { coreMock } from '@kbn/core/public/mocks';
+import { DashboardTopNav } from '@kbn/dashboard-plugin/public';
+import { ViewMode } from '@kbn/embeddable-plugin/public';
+import { APP_NAME } from '../../../common/constants';
+import { NavigationProvider, SecurityPageName } from '@kbn/security-solution-navigation';
+import { TestProviders } from '../../common/mock';
+import { useNavigation } from '../../common/lib/kibana';
+
+const mockDashboardTopNav = DashboardTopNav as jest.Mock;
+
+jest.mock('../../common/lib/kibana', () => {
+ const actual = jest.requireActual('../../common/lib/kibana');
+ return {
+ ...actual,
+ useNavigation: jest.fn(),
+ useCapabilities: jest.fn(() => ({ showWriteControls: true })),
+ };
+});
+jest.mock('../../common/components/link_to', () => ({ useGetSecuritySolutionUrl: jest.fn() }));
+jest.mock('@kbn/dashboard-plugin/public', () => ({
+ DashboardTopNav: jest.fn(() => ),
+}));
+const mockCore = coreMock.createStart();
+const mockNavigateTo = jest.fn();
+const mockGetAppUrl = jest.fn();
+const mockDashboardContainer = {
+ select: jest.fn(),
+} as unknown as DashboardAPI;
+
+const wrapper = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+describe('DashboardToolBar', () => {
+ const mockOnLoad = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (useNavigation as jest.Mock).mockReturnValue({
+ navigateTo: mockNavigateTo,
+ getAppUrl: mockGetAppUrl,
+ });
+ render(, {
+ wrapper,
+ });
+ });
+ it('should render the DashboardToolBar component', () => {
+ expect(screen.getByTestId('dashboard-top-nav')).toBeInTheDocument();
+ });
+
+ it('should render the DashboardToolBar component with the correct props for view mode', () => {
+ expect(mockOnLoad).toHaveBeenCalledWith(ViewMode.VIEW);
+ });
+
+ it('should render the DashboardTopNav component with the correct redirect to listing url', () => {
+ mockDashboardTopNav.mock.calls[0][0].redirectTo({ destination: 'listing' });
+ });
+
+ it('should render the DashboardTopNav component with the correct breadcrumb', () => {
+ expect(mockGetAppUrl.mock.calls[0][0].deepLinkId).toEqual(SecurityPageName.landing);
+ expect(mockDashboardTopNav.mock.calls[0][0].customLeadingBreadCrumbs[0].text).toEqual(APP_NAME);
+ });
+
+ it('should render the DashboardTopNav component with the correct redirect to create dashboard url', () => {
+ mockDashboardTopNav.mock.calls[0][0].redirectTo({ destination: 'dashboard' });
+
+ expect(mockNavigateTo.mock.calls[0][0].deepLinkId).toEqual(SecurityPageName.dashboards);
+ expect(mockNavigateTo.mock.calls[0][0].path).toEqual(`/create`);
+ });
+
+ it('should render the DashboardTopNav component with the correct redirect to edit dashboard url', () => {
+ const mockDashboardId = 'dashboard123';
+
+ mockDashboardTopNav.mock.calls[0][0].redirectTo({
+ destination: 'dashboard',
+ id: mockDashboardId,
+ });
+ expect(mockNavigateTo.mock.calls[0][0].deepLinkId).toEqual(SecurityPageName.dashboards);
+ expect(mockNavigateTo.mock.calls[0][0].path).toEqual(`${mockDashboardId}/edit`);
+ });
+
+ it('should render the DashboardTopNav component with the correct props', () => {
+ expect(mockDashboardTopNav.mock.calls[0][0].embedSettings).toEqual(
+ expect.objectContaining({
+ forceHideFilterBar: true,
+ forceShowTopNavMenu: true,
+ forceShowDatePicker: false,
+ forceShowQueryInput: false,
+ })
+ );
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.tsx b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.tsx
new file mode 100644
index 0000000000000..eb74f7c563500
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/dashboards/components/dashboard_tool_bar.tsx
@@ -0,0 +1,86 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback, useEffect, useMemo } from 'react';
+import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
+import { DashboardTopNav, LEGACY_DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public';
+import { ViewMode } from '@kbn/embeddable-plugin/public';
+
+import type { ChromeBreadcrumb } from '@kbn/core/public';
+import type { DashboardCapabilities } from '@kbn/dashboard-plugin/common';
+import { SecurityPageName } from '../../../common';
+import { useCapabilities, useKibana, useNavigation } from '../../common/lib/kibana';
+import { APP_NAME } from '../../../common/constants';
+
+const DashboardToolBarComponent = ({
+ dashboardContainer,
+ onLoad,
+}: {
+ dashboardContainer: DashboardAPI;
+ onLoad?: (mode: ViewMode) => void;
+}) => {
+ const { setHeaderActionMenu } = useKibana().services;
+
+ const viewMode =
+ dashboardContainer?.select((state) => state.explicitInput.viewMode) ?? ViewMode.VIEW;
+
+ const { navigateTo, getAppUrl } = useNavigation();
+ const redirectTo = useCallback(
+ ({ destination, id }) => {
+ if (destination === 'listing') {
+ navigateTo({ deepLinkId: SecurityPageName.dashboards });
+ }
+ if (destination === 'dashboard') {
+ navigateTo({
+ deepLinkId: SecurityPageName.dashboards,
+ path: id ? `${id}/edit` : `/create`,
+ });
+ }
+ },
+ [navigateTo]
+ );
+
+ const landingBreadcrumb: ChromeBreadcrumb[] = useMemo(
+ () => [
+ {
+ text: APP_NAME,
+ href: getAppUrl({ deepLinkId: SecurityPageName.landing }),
+ },
+ ],
+ [getAppUrl]
+ );
+
+ useEffect(() => {
+ onLoad?.(viewMode);
+ }, [onLoad, viewMode]);
+
+ const embedSettings = useMemo(
+ () => ({
+ forceHideFilterBar: true,
+ forceShowTopNavMenu: true,
+ forceShowQueryInput: false,
+ forceShowDatePicker: false,
+ }),
+ []
+ );
+ const { showWriteControls } = useCapabilities(LEGACY_DASHBOARD_APP_ID);
+
+ return showWriteControls ? (
+
+ ) : null;
+};
+
+export const DashboardToolBar = React.memo(DashboardToolBarComponent);
diff --git a/x-pack/plugins/security_solution/public/dashboards/components/edit_dashboard_button.test.tsx b/x-pack/plugins/security_solution/public/dashboards/components/edit_dashboard_button.test.tsx
deleted file mode 100644
index 43afa552d50fd..0000000000000
--- a/x-pack/plugins/security_solution/public/dashboards/components/edit_dashboard_button.test.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import type { RenderResult } from '@testing-library/react';
-import { fireEvent, render } from '@testing-library/react';
-import React from 'react';
-import type { Query } from '@kbn/es-query';
-
-import { useKibana } from '../../common/lib/kibana';
-import { TestProviders } from '../../common/mock/test_providers';
-import type { EditDashboardButtonComponentProps } from './edit_dashboard_button';
-import { EditDashboardButton } from './edit_dashboard_button';
-import { ViewMode } from '@kbn/embeddable-plugin/public';
-
-jest.mock('../../common/lib/kibana/kibana_react', () => {
- return {
- useKibana: jest.fn(),
- };
-});
-
-describe('EditDashboardButton', () => {
- const timeRange = {
- from: '2023-03-24T00:00:00.000Z',
- to: '2023-03-24T23:59:59.999Z',
- };
-
- const props = {
- filters: [],
- query: { query: '', language: '' } as Query,
- savedObjectId: 'mockSavedObjectId',
- timeRange,
- };
- const servicesMock = {
- dashboard: { locator: { getRedirectUrl: jest.fn() } },
- application: {
- navigateToApp: jest.fn(),
- navigateToUrl: jest.fn(),
- },
- };
-
- const renderButton = (testProps: EditDashboardButtonComponentProps) => {
- return render(
-
-
-
- );
- };
-
- let renderResult: RenderResult;
- beforeEach(() => {
- (useKibana as jest.Mock).mockReturnValue({
- services: servicesMock,
- });
- renderResult = renderButton(props);
- });
-
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('should render', () => {
- expect(renderResult.queryByTestId('dashboardEditButton')).toBeInTheDocument();
- });
-
- it('should render dashboard edit url', () => {
- fireEvent.click(renderResult.getByTestId('dashboardEditButton'));
- expect(servicesMock.dashboard?.locator?.getRedirectUrl).toHaveBeenCalledWith(
- expect.objectContaining({
- query: props.query,
- filters: props.filters,
- timeRange: props.timeRange,
- dashboardId: props.savedObjectId,
- viewMode: ViewMode.EDIT,
- })
- );
- });
-});
diff --git a/x-pack/plugins/security_solution/public/dashboards/components/edit_dashboard_button.tsx b/x-pack/plugins/security_solution/public/dashboards/components/edit_dashboard_button.tsx
deleted file mode 100644
index bd360229c7e1f..0000000000000
--- a/x-pack/plugins/security_solution/public/dashboards/components/edit_dashboard_button.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useCallback } from 'react';
-import type { Query, Filter } from '@kbn/es-query';
-import { EuiButton } from '@elastic/eui';
-import { ViewMode } from '@kbn/embeddable-plugin/public';
-import { EDIT_DASHBOARD_BUTTON_TITLE } from '../pages/details/translations';
-import { useKibana, useNavigation } from '../../common/lib/kibana';
-
-export interface EditDashboardButtonComponentProps {
- filters?: Filter[];
- query?: Query;
- savedObjectId: string | undefined;
- timeRange: {
- from: string;
- to: string;
- fromStr?: string | undefined;
- toStr?: string | undefined;
- };
-}
-
-const EditDashboardButtonComponent: React.FC = ({
- filters,
- query,
- savedObjectId,
- timeRange,
-}) => {
- const {
- services: { dashboard },
- } = useKibana();
- const { navigateTo } = useNavigation();
-
- const onClick = useCallback(
- (e) => {
- e.preventDefault();
- const url = dashboard?.locator?.getRedirectUrl({
- query,
- filters,
- timeRange,
- dashboardId: savedObjectId,
- viewMode: ViewMode.EDIT,
- });
- if (url) {
- navigateTo({ url });
- }
- },
- [dashboard?.locator, query, filters, timeRange, savedObjectId, navigateTo]
- );
- return (
-
- {EDIT_DASHBOARD_BUTTON_TITLE}
-
- );
-};
-
-EditDashboardButtonComponent.displayName = 'EditDashboardComponent';
-export const EditDashboardButton = React.memo(EditDashboardButtonComponent);
diff --git a/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx b/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx
index cb8b40b1c0907..02bccd69eb253 100644
--- a/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx
@@ -20,7 +20,6 @@ const DashboardContext = React.createContext({ secu
export const DashboardContextProvider: React.FC = ({ children }) => {
const { tags, isLoading } = useFetchSecurityTags();
-
const securityTags = isLoading || !tags ? null : tags;
return {children};
diff --git a/x-pack/plugins/security_solution/public/dashboards/hooks/translations.ts b/x-pack/plugins/security_solution/public/dashboards/hooks/translations.ts
deleted file mode 100644
index 58254aa8fe9f6..0000000000000
--- a/x-pack/plugins/security_solution/public/dashboards/hooks/translations.ts
+++ /dev/null
@@ -1,19 +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 { i18n } from '@kbn/i18n';
-
-export const DASHBOARD_TITLE = i18n.translate('xpack.securitySolution.dashboards.title', {
- defaultMessage: 'Title',
-});
-
-export const DASHBOARDS_DESCRIPTION = i18n.translate(
- 'xpack.securitySolution.dashboards.description',
- {
- defaultMessage: 'Description',
- }
-);
diff --git a/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.test.ts b/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.test.tsx
similarity index 69%
rename from x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.test.ts
rename to x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.test.tsx
index 47509521e5574..52540aff6aa7e 100644
--- a/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.test.ts
+++ b/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.test.tsx
@@ -6,23 +6,37 @@
*/
import { renderHook, act } from '@testing-library/react-hooks';
-import type { DashboardStart } from '@kbn/dashboard-plugin/public';
import { useKibana } from '../../common/lib/kibana';
import { useCreateSecurityDashboardLink } from './use_create_security_dashboard_link';
import { DashboardContextProvider } from '../context/dashboard_context';
import { getTagsByName } from '../../common/containers/tags/api';
+import React from 'react';
+import { TestProviders } from '../../common/mock';
-jest.mock('../../common/lib/kibana');
+jest.mock('@kbn/security-solution-navigation/src/context');
+jest.mock('../../common/lib/kibana', () => ({
+ useKibana: jest.fn(),
+}));
jest.mock('../../common/containers/tags/api');
-const URL = '/path';
+jest.mock('../../common/lib/apm/use_track_http_request');
+jest.mock('../../common/components/link_to', () => ({
+ useGetSecuritySolutionUrl: jest
+ .fn()
+ .mockReturnValue(jest.fn().mockReturnValue('/app/security/dashboards/create')),
+}));
const renderUseCreateSecurityDashboardLink = () =>
renderHook(() => useCreateSecurityDashboardLink(), {
- wrapper: DashboardContextProvider,
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
});
const asyncRenderUseCreateSecurityDashboard = async () => {
const renderedHook = renderUseCreateSecurityDashboardLink();
+
await act(async () => {
await renderedHook.waitForNextUpdate();
});
@@ -30,12 +44,15 @@ const asyncRenderUseCreateSecurityDashboard = async () => {
};
describe('useCreateSecurityDashboardLink', () => {
- const mockGetRedirectUrl = jest.fn(() => URL);
-
beforeAll(() => {
- useKibana().services.dashboard = {
- locator: { getRedirectUrl: mockGetRedirectUrl },
- } as unknown as DashboardStart;
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ savedObjectsTagging: {
+ create: jest.fn(),
+ },
+ http: { get: jest.fn() },
+ },
+ });
});
afterEach(() => {
@@ -55,8 +72,7 @@ describe('useCreateSecurityDashboardLink', () => {
const result1 = result.current;
act(() => rerender());
const result2 = result.current;
-
- expect(result1).toBe(result2);
+ expect(result1).toEqual(result2);
});
it('should not re-request tag id when re-rendered', async () => {
@@ -71,14 +87,14 @@ describe('useCreateSecurityDashboardLink', () => {
const { result, waitForNextUpdate } = renderUseCreateSecurityDashboardLink();
expect(result.current.isLoading).toEqual(true);
- expect(result.current.url).toEqual('');
+ expect(result.current.url).toEqual('/app/security/dashboards/create');
await act(async () => {
await waitForNextUpdate();
});
expect(result.current.isLoading).toEqual(false);
- expect(result.current.url).toEqual(URL);
+ expect(result.current.url).toEqual('/app/security/dashboards/create');
});
});
});
diff --git a/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.ts b/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.ts
index 633d9d01efe17..24f91eec5211c 100644
--- a/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.ts
+++ b/x-pack/plugins/security_solution/public/dashboards/hooks/use_create_security_dashboard_link.ts
@@ -7,24 +7,28 @@
import { useMemo } from 'react';
import { useSecurityTags } from '../context/dashboard_context';
-import { useKibana } from '../../common/lib/kibana';
+import { useGetSecuritySolutionUrl } from '../../common/components/link_to';
+import { SecurityPageName } from '../../../common';
type UseCreateDashboard = () => { isLoading: boolean; url: string };
export const useCreateSecurityDashboardLink: UseCreateDashboard = () => {
- const { dashboard } = useKibana().services;
+ const getSecuritySolutionUrl = useGetSecuritySolutionUrl();
const securityTags = useSecurityTags();
-
+ const url = getSecuritySolutionUrl({
+ deepLinkId: SecurityPageName.dashboards,
+ path: 'create',
+ });
const result = useMemo(() => {
const firstSecurityTagId = securityTags?.[0]?.id;
if (!firstSecurityTagId) {
- return { isLoading: true, url: '' };
+ return { isLoading: true, url };
}
return {
isLoading: false,
- url: dashboard?.locator?.getRedirectUrl({ tags: [firstSecurityTagId] }) ?? '',
+ url,
};
- }, [securityTags, dashboard?.locator]);
+ }, [securityTags, url]);
return result;
};
diff --git a/x-pack/plugins/security_solution/public/dashboards/hooks/use_dashboard_renderer.test.tsx b/x-pack/plugins/security_solution/public/dashboards/hooks/use_dashboard_renderer.test.tsx
new file mode 100644
index 0000000000000..d8f5bae2361c8
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/dashboards/hooks/use_dashboard_renderer.test.tsx
@@ -0,0 +1,26 @@
+/*
+ * 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 type { DashboardAPI } from '@kbn/dashboard-plugin/public';
+
+import { useDashboardRenderer } from './use_dashboard_renderer';
+
+jest.mock('../../common/lib/kibana');
+
+const mockDashboardContainer = { getExplicitInput: () => ({ tags: ['tagId'] }) } as DashboardAPI;
+
+describe('useDashboardRenderer', () => {
+ it('should set dashboard container correctly when dashboard is loaded', async () => {
+ const { result } = renderHook(() => useDashboardRenderer());
+
+ await act(async () => {
+ await result.current.handleDashboardLoaded(mockDashboardContainer);
+ });
+
+ expect(result.current.dashboardContainer).toEqual(mockDashboardContainer);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/dashboards/hooks/use_dashboard_renderer.tsx b/x-pack/plugins/security_solution/public/dashboards/hooks/use_dashboard_renderer.tsx
new file mode 100644
index 0000000000000..104692e62f2bb
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/dashboards/hooks/use_dashboard_renderer.tsx
@@ -0,0 +1,25 @@
+/*
+ * 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 { useCallback, useMemo, useState } from 'react';
+import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
+
+export const useDashboardRenderer = () => {
+ const [dashboardContainer, setDashboardContainer] = useState();
+
+ const handleDashboardLoaded = useCallback((container: DashboardAPI) => {
+ setDashboardContainer(container);
+ }, []);
+
+ return useMemo(
+ () => ({
+ dashboardContainer,
+ handleDashboardLoaded,
+ }),
+ [dashboardContainer, handleDashboardLoaded]
+ );
+};
diff --git a/x-pack/plugins/security_solution/public/dashboards/hooks/use_security_dashboards_table.tsx b/x-pack/plugins/security_solution/public/dashboards/hooks/use_security_dashboards_table.tsx
index 7f58f804d488c..07446294ab203 100644
--- a/x-pack/plugins/security_solution/public/dashboards/hooks/use_security_dashboards_table.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/hooks/use_security_dashboards_table.tsx
@@ -8,9 +8,9 @@
import React, { useMemo, useCallback } from 'react';
import type { MouseEventHandler } from 'react';
import type { EuiBasicTableColumn } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { LinkAnchor } from '../../common/components/links';
import { useKibana, useNavigateTo } from '../../common/lib/kibana';
-import * as i18n from './translations';
import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../common/lib/telemetry';
import { SecurityPageName } from '../../../common/constants';
import { useGetSecuritySolutionUrl } from '../../common/components/link_to';
@@ -56,7 +56,9 @@ export const useSecurityDashboardsTableColumns = (): Array<
(): Array> => [
{
field: 'title',
- name: i18n.DASHBOARD_TITLE,
+ name: i18n.translate('xpack.securitySolution.dashboards.title', {
+ defaultMessage: 'Title',
+ }),
sortable: true,
render: (title: string, { id }) => {
const href = `${getSecuritySolutionUrl({
@@ -75,7 +77,9 @@ export const useSecurityDashboardsTableColumns = (): Array<
},
{
field: 'description',
- name: i18n.DASHBOARDS_DESCRIPTION,
+ name: i18n.translate('xpack.securitySolution.dashboards.description', {
+ defaultMessage: 'Description',
+ }),
sortable: true,
render: (description: string) => description || getEmptyValue(),
'data-test-subj': 'dashboardTableDescriptionCell',
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/breadcrumbs.ts b/x-pack/plugins/security_solution/public/dashboards/pages/breadcrumbs.ts
index 01e663e8abb7e..f59369cb4e755 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/breadcrumbs.ts
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/breadcrumbs.ts
@@ -5,7 +5,9 @@
* 2.0.
*/
+import { matchPath } from 'react-router-dom';
import type { GetTrailingBreadcrumbs } from '../../common/components/navigation/breadcrumbs/types';
+import { CREATE_DASHBOARD_TITLE } from './translations';
/**
* This module should only export this function.
@@ -13,6 +15,10 @@ import type { GetTrailingBreadcrumbs } from '../../common/components/navigation/
* We should be careful to not import unnecessary modules in this file to avoid increasing the main app bundle size.
*/
export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = (params, getSecuritySolutionUrl) => {
+ if (matchPath(params.pathName, { path: '/create' })) {
+ return [{ text: CREATE_DASHBOARD_TITLE }];
+ }
+
const breadcrumbName = params?.state?.dashboardName;
if (breadcrumbName) {
return [{ text: breadcrumbName }];
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/details/index.test.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/details/index.test.tsx
index 9411bda35a632..3c85a18f2d3aa 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/details/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/details/index.test.tsx
@@ -11,6 +11,7 @@ import { Router } from '@kbn/shared-ux-router';
import { DashboardView } from '.';
import { useCapabilities } from '../../../common/lib/kibana';
import { TestProviders } from '../../../common/mock';
+import { ViewMode } from '@kbn/embeddable-plugin/public';
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
@@ -68,7 +69,7 @@ describe('DashboardView', () => {
test('render when no error state', () => {
const { queryByTestId } = render(
-
+
,
{ wrapper: TestProviders }
);
@@ -83,7 +84,7 @@ describe('DashboardView', () => {
});
const { queryByTestId } = render(
-
+
,
{ wrapper: TestProviders }
);
@@ -95,7 +96,7 @@ describe('DashboardView', () => {
test('render dashboard view with height', () => {
const { queryByTestId } = render(
-
+
,
{ wrapper: TestProviders }
);
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/details/index.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/details/index.tsx
index 8bbfec9f99218..6f07b377a22d0 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/details/index.tsx
@@ -7,13 +7,12 @@
import React, { useState, useCallback, useMemo } from 'react';
import { LEGACY_DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public';
-import type { DashboardAPI } from '@kbn/dashboard-plugin/public';
import type { DashboardCapabilities } from '@kbn/dashboard-plugin/common/types';
import { useParams } from 'react-router-dom';
-
import { pick } from 'lodash/fp';
-import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import type { ViewMode } from '@kbn/embeddable-plugin/common';
import { SecurityPageName } from '../../../../common/constants';
import { SpyRoute } from '../../../common/utils/route/spy_routes';
import { useCapabilities } from '../../../common/lib/kibana';
@@ -26,16 +25,22 @@ import { FiltersGlobal } from '../../../common/components/filters_global';
import { InputsModelId } from '../../../common/store/inputs/constants';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { HeaderPage } from '../../../common/components/header_page';
-import { DASHBOARD_NOT_FOUND_TITLE } from './translations';
import { inputsSelectors } from '../../../common/store';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
-import { EditDashboardButton } from '../../components/edit_dashboard_button';
+import { DashboardToolBar } from '../../components/dashboard_tool_bar';
+
+import { useDashboardRenderer } from '../../hooks/use_dashboard_renderer';
+import { DashboardTitle } from '../../components/dashboard_title';
-type DashboardDetails = Record;
+interface DashboardViewProps {
+ initialViewMode: ViewMode;
+}
const dashboardViewFlexGroupStyle = { minHeight: `calc(100vh - 140px)` };
-const DashboardViewComponent: React.FC = () => {
+const DashboardViewComponent: React.FC = ({
+ initialViewMode,
+}: DashboardViewProps) => {
const { fromStr, toStr, from, to } = useDeepEqualSelector((state) =>
pick(['fromStr', 'toStr', 'from', 'to'], inputsSelectors.globalTimeRangeSelector(state))
);
@@ -47,36 +52,28 @@ const DashboardViewComponent: React.FC = () => {
);
const query = useDeepEqualSelector(getGlobalQuerySelector);
const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector);
- const { indexPattern, indicesExist } = useSourcererDataView();
+ const { indexPattern } = useSourcererDataView();
- const { show: canReadDashboard, showWriteControls } =
+ const { show: canReadDashboard } =
useCapabilities(LEGACY_DASHBOARD_APP_ID);
const errorState = useMemo(
() => (canReadDashboard ? null : DashboardViewPromptState.NoReadPermission),
[canReadDashboard]
);
- const [dashboardDetails, setDashboardDetails] = useState();
- const onDashboardContainerLoaded = useCallback((dashboard: DashboardAPI) => {
- if (dashboard) {
- const title = dashboard.getTitle().trim();
- if (title) {
- setDashboardDetails({ title });
- } else {
- setDashboardDetails({ title: DASHBOARD_NOT_FOUND_TITLE });
- }
- }
- }, []);
-
- const dashboardExists = useMemo(() => dashboardDetails != null, [dashboardDetails]);
+ const [viewMode, setViewMode] = useState(initialViewMode);
const { detailName: savedObjectId } = useParams<{ detailName?: string }>();
+ const [dashboardTitle, setDashboardTitle] = useState();
+
+ const { dashboardContainer, handleDashboardLoaded } = useDashboardRenderer();
+ const onDashboardToolBarLoad = useCallback((mode: ViewMode) => {
+ setViewMode(mode);
+ }, []);
return (
<>
- {indicesExist && (
-
-
-
- )}
+
+
+
{
data-test-subj="dashboard-view-wrapper"
>
- }>
- {showWriteControls && dashboardExists && (
-
- )}
-
+ {dashboardContainer && (
+
+ }
+ subtitle={
+
+ }
+ />
+ )}
{!errorState && (
@@ -102,10 +106,12 @@ const DashboardViewComponent: React.FC = () => {
query={query}
filters={filters}
canReadDashboard={canReadDashboard}
+ dashboardContainer={dashboardContainer}
id={`dashboard-view-${savedObjectId}`}
- onDashboardContainerLoaded={onDashboardContainerLoaded}
+ onDashboardContainerLoaded={handleDashboardLoaded}
savedObjectId={savedObjectId}
timeRange={timeRange}
+ viewMode={viewMode}
/>
)}
@@ -116,7 +122,7 @@ const DashboardViewComponent: React.FC = () => {
)}
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/details/translations.ts b/x-pack/plugins/security_solution/public/dashboards/pages/details/translations.ts
index ddfa94bd75584..a760d79a8e30d 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/details/translations.ts
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/details/translations.ts
@@ -40,3 +40,24 @@ export const EDIT_DASHBOARD_BUTTON_TITLE = i18n.translate(
defaultMessage: `Edit`,
}
);
+
+export const EDIT_DASHBOARD_TITLE = i18n.translate(
+ 'xpack.securitySolution.dashboards.dashboard.editDashboardTitle',
+ {
+ defaultMessage: `Editing new dashboard`,
+ }
+);
+
+export const VIEW_DASHBOARD_BUTTON_TITLE = i18n.translate(
+ 'xpack.securitySolution.dashboards.dashboard.viewDashboardButtonTitle',
+ {
+ defaultMessage: `Switch to view mode`,
+ }
+);
+
+export const SAVE_DASHBOARD_BUTTON_TITLE = i18n.translate(
+ 'xpack.securitySolution.dashboards.dashboard.saveDashboardButtonTitle',
+ {
+ defaultMessage: `Save`,
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/index.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/index.tsx
index 0bd521f47a69c..993a4b37a1ec7 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/index.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/index.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { Routes, Route } from '@kbn/shared-ux-router';
+import { ViewMode } from '@kbn/embeddable-plugin/public';
import { DashboardsLandingPage } from './landing_page';
import { DashboardView } from './details';
import { DASHBOARDS_PATH } from '../../../common/constants';
@@ -16,8 +17,14 @@ const DashboardsContainerComponent = () => {
return (
+
+
+
+
+
+
-
+
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx
index ac26f9038d5d4..8723bfb69f326 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx
@@ -23,13 +23,10 @@ import { DASHBOARDS_PAGE_SECTION_CUSTOM } from './translations';
jest.mock('../../../common/containers/tags/api');
jest.mock('../../../common/lib/kibana');
jest.mock('../../../common/utils/route/spy_routes', () => ({ SpyRoute: () => null }));
-jest.mock('@kbn/dashboard-plugin/public', () => {
- const actual = jest.requireActual('@kbn/dashboard-plugin/public');
- return {
- ...actual,
- DashboardListingTable: jest.fn().mockReturnValue(),
- };
-});
+jest.mock('@kbn/dashboard-plugin/public', () => ({
+ DashboardListingTable: jest.fn().mockReturnValue(),
+ DashboardTopNav: jest.fn().mockReturnValue(),
+}));
const mockUseObservable = jest.fn();
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx
index 2af7ef2f9902b..10fb3c060548f 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx
@@ -99,20 +99,18 @@ export const DashboardsLandingPage = () => {
})}`,
[getSecuritySolutionUrl]
);
- const { isLoading: loadingCreateDashboardUrl, url: createDashboardUrl } =
- useCreateSecurityDashboardLink();
-
- const getHref = useCallback(
- (id: string | undefined) => (id ? getSecuritySolutionDashboardUrl(id) : createDashboardUrl),
- [createDashboardUrl, getSecuritySolutionDashboardUrl]
- );
const goToDashboard = useCallback(
(dashboardId: string | undefined) => {
track(METRIC_TYPE.CLICK, TELEMETRY_EVENT.DASHBOARD);
- navigateTo({ url: getHref(dashboardId) });
+ navigateTo({
+ url: getSecuritySolutionUrl({
+ deepLinkId: SecurityPageName.dashboards,
+ path: dashboardId ?? 'create',
+ }),
+ });
},
- [getHref, navigateTo]
+ [getSecuritySolutionUrl, navigateTo]
);
const securityTags = useSecurityTags();
@@ -151,7 +149,7 @@ export const DashboardsLandingPage = () => {
void;
+ rule: Rule | null;
+ 'data-test-subj': string;
+}
+
+export const EndpointExceptionsViewer = memo(
+ ({
+ isViewReadOnly,
+ onRuleChange,
+ rule,
+ 'data-test-subj': dataTestSubj,
+ }: EndpointExceptionsViewerProps) => {
+ const EndpointExceptionsUnavailableComponent = useGetEndpointExceptionsUnavailableComponent();
+ return (
+ <>
+ {!EndpointExceptionsUnavailableComponent ? (
+
+ ) : (
+
+
+
+
+
+ )}
+ >
+ );
+ }
+);
+
+EndpointExceptionsViewer.displayName = 'EndpointExceptionsViewer';
diff --git a/x-pack/plugins/security_solution/public/detection_engine/endpoint_exceptions/use_get_endpoint_exceptions_unavailablle_component.tsx b/x-pack/plugins/security_solution/public/detection_engine/endpoint_exceptions/use_get_endpoint_exceptions_unavailablle_component.tsx
new file mode 100644
index 0000000000000..a3b0afb2caba5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/endpoint_exceptions/use_get_endpoint_exceptions_unavailablle_component.tsx
@@ -0,0 +1,13 @@
+/*
+ * 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 React from 'react';
+import { useUpsellingComponent } from '../../common/hooks/use_upselling';
+
+export const useGetEndpointExceptionsUnavailableComponent = (): React.ComponentType | null => {
+ return useUpsellingComponent('ruleDetailsEndpointExceptions');
+};
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_search_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_search_bar.tsx
index 0fd306e413912..3622182aa7fc3 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_search_bar.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/execution_log_table/execution_log_search_bar.tsx
@@ -9,7 +9,8 @@ import React, { useCallback } from 'react';
import { replace } from 'lodash';
import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { RuleExecutionStatus } from '../../../../../../common/api/detection_engine/rule_monitoring';
+import type { RuleExecutionStatus } from '../../../../../../common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '../../../../../../common/api/detection_engine/rule_monitoring';
import { ExecutionStatusFilter } from '../../../../rule_monitoring';
import * as i18n from './translations';
@@ -36,10 +37,10 @@ export const replaceQueryTextAliases = (queryText: string): string => {
};
// This only includes statuses which are or can be final
-const STATUS_FILTERS = [
- RuleExecutionStatus.succeeded,
- RuleExecutionStatus.failed,
- RuleExecutionStatus['partial failure'],
+const STATUS_FILTERS: RuleExecutionStatus[] = [
+ RuleExecutionStatusEnum.succeeded,
+ RuleExecutionStatusEnum.failed,
+ RuleExecutionStatusEnum['partial failure'],
];
interface ExecutionLogTableSearchProps {
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx
index 97f02b8e35cf2..223593ef3e095 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx
@@ -19,7 +19,7 @@ import {
EuiWindowEvent,
} from '@elastic/eui';
import type { Filter } from '@kbn/es-query';
-import { Routes, Route } from '@kbn/shared-ux-router';
+import { Route, Routes } from '@kbn/shared-ux-router';
import { noop } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -32,12 +32,13 @@ import type { Dispatch } from 'redux';
import { isTab } from '@kbn/timelines-plugin/public';
import {
- tableDefaults,
dataTableActions,
dataTableSelectors,
FILTER_OPEN,
+ tableDefaults,
TableId,
} from '@kbn/securitysolution-data-table';
+import { EndpointExceptionsViewer } from '../../../endpoint_exceptions/endpoint_exceptions_viewer';
import { AlertsTableComponent } from '../../../../detections/components/alerts_table';
import { GroupedAlertsTable } from '../../../../detections/components/alerts_table/alerts_grouping';
import { useDataTableFilters } from '../../../../common/hooks/use_data_table_filters';
@@ -100,10 +101,10 @@ import {
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import {
- explainLackOfPermission,
canEditRuleWithActions,
- isBoolean,
+ explainLackOfPermission,
hasUserCRUDPermission,
+ isBoolean,
} from '../../../../common/utils/privileges';
import {
@@ -149,8 +150,6 @@ const RULE_EXCEPTION_LIST_TYPES = [
ExceptionListTypeEnum.RULE_DEFAULT,
];
-const RULE_ENDPOINT_EXCEPTION_LIST_TYPE = [ExceptionListTypeEnum.ENDPOINT];
-
/**
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
*/
@@ -780,9 +779,8 @@ const RuleDetailsPageComponent: React.FC = ({
- {
beforeAll(() => {
- (useRuleExecutionSettings as jest.Mock).mockReturnValue({
+ mockUseRuleExecutionSettings.mockReturnValue({
extendedLogging: {
isEnabled: false,
minLevel: 'debug',
},
});
+ mockUseEndpointExceptionsCapability.mockReturnValue(true);
});
beforeEach(() => {
@@ -119,6 +125,32 @@ describe('useRuleDetailsTabs', () => {
expect(tabsNames).toContain(RuleDetailTabs.endpointExceptions);
});
+ it('hides endpoint exceptions tab when rule includes endpoint list but no endpoint PLI', async () => {
+ mockUseEndpointExceptionsCapability.mockReturnValue(false);
+ const tabs = render({
+ rule: {
+ ...mockRule,
+ outcome: 'conflict',
+ alias_target_id: 'aliased_rule_id',
+ alias_purpose: 'savedObjectConversion',
+ exceptions_list: [
+ {
+ id: 'endpoint_list',
+ list_id: 'endpoint_list',
+ type: 'endpoint',
+ namespace_type: 'agnostic',
+ },
+ ],
+ },
+ ruleId: mockRule.rule_id,
+ isExistingRule: true,
+ hasIndexRead: true,
+ });
+ const tabsNames = Object.keys(tabs.result.current);
+
+ expect(tabsNames).not.toContain(RuleDetailTabs.endpointExceptions);
+ });
+
it('does not return the execution events tab if extended logging is disabled', async () => {
const tabs = render({
rule: mockRule,
@@ -132,7 +164,7 @@ describe('useRuleDetailsTabs', () => {
});
it('returns the execution events tab if extended logging is enabled', async () => {
- (useRuleExecutionSettings as jest.Mock).mockReturnValue({
+ mockUseRuleExecutionSettings.mockReturnValue({
extendedLogging: {
isEnabled: true,
minLevel: 'debug',
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/use_rule_details_tabs.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/use_rule_details_tabs.tsx
index 42a42caeb732d..8c73bafd049aa 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/use_rule_details_tabs.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/use_rule_details_tabs.tsx
@@ -8,6 +8,7 @@
import { useEffect, useMemo, useState } from 'react';
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { omit } from 'lodash/fp';
+import { useEndpointExceptionsCapability } from '../../../../exceptions/hooks/use_endpoint_exceptions_capability';
import * as detectionI18n from '../../../../detections/pages/detection_engine/translations';
import * as i18n from './translations';
import type { Rule } from '../../../rule_management/logic';
@@ -80,9 +81,10 @@ export const useRuleDetailsTabs = ({
);
const [pageTabs, setTabs] = useState>>(ruleDetailTabs);
-
const ruleExecutionSettings = useRuleExecutionSettings();
+ const canReadEndpointExceptions = useEndpointExceptionsCapability('showEndpointExceptions');
+
useEffect(() => {
const hiddenTabs = [];
@@ -92,6 +94,9 @@ export const useRuleDetailsTabs = ({
if (!ruleExecutionSettings.extendedLogging.isEnabled) {
hiddenTabs.push(RuleDetailTabs.executionEvents);
}
+ if (!canReadEndpointExceptions) {
+ hiddenTabs.push(RuleDetailTabs.endpointExceptions);
+ }
if (rule != null) {
const hasEndpointList = (rule.exceptions_list ?? []).some(
(list) => list.type === ExceptionListTypeEnum.ENDPOINT
@@ -104,7 +109,7 @@ export const useRuleDetailsTabs = ({
const tabs = omit>(hiddenTabs, ruleDetailTabs);
setTabs(tabs);
- }, [hasIndexRead, rule, ruleDetailTabs, ruleExecutionSettings]);
+ }, [canReadEndpointExceptions, hasIndexRead, rule, ruleDetailTabs, ruleExecutionSettings]);
return pageTabs;
};
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx
index 1d1dd112da377..05e6e0513f741 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx
@@ -19,7 +19,9 @@ import type { Rule } from '../../../rule_management/logic/types';
import { mockRule } from '../../../rule_management_ui/components/rules_table/__mocks__/mock';
import { useFindExceptionListReferences } from '../../logic/use_find_references';
import * as i18n from './translations';
+import { useEndpointExceptionsCapability } from '../../../../exceptions/hooks/use_endpoint_exceptions_capability';
+jest.mock('../../../../exceptions/hooks/use_endpoint_exceptions_capability');
jest.mock('../../../../common/lib/kibana');
jest.mock('@kbn/securitysolution-list-hooks');
jest.mock('@kbn/securitysolution-list-api');
@@ -29,6 +31,8 @@ jest.mock('react', () => {
return { ...r, useReducer: jest.fn() };
});
+const mockUseEndpointExceptionsCapability = useEndpointExceptionsCapability as jest.Mock;
+
const sampleExceptionItem = {
_version: 'WzEwMjM4MSwxXQ==',
comments: [],
@@ -81,6 +85,8 @@ describe('ExceptionsViewer', () => {
},
});
+ mockUseEndpointExceptionsCapability.mockReturnValue(true);
+
(fetchExceptionListsItemsByListIds as jest.Mock).mockReturnValue({ total: 0 });
(useFindExceptionListReferences as jest.Mock).mockReturnValue([
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx
index df6a6c2835a1d..cc86bbf02291b 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx
@@ -5,18 +5,18 @@
* 2.0.
*/
-import React, { useCallback, useMemo, useEffect, useReducer } from 'react';
+import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
import styled from 'styled-components';
import { EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
-import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import type {
ExceptionListItemSchema,
- UseExceptionListItemsSuccess,
- Pagination,
ExceptionListSchema,
+ Pagination,
+ UseExceptionListItemsSuccess,
} from '@kbn/securitysolution-io-ts-list-types';
+import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { transformInput } from '@kbn/securitysolution-list-hooks';
import {
@@ -29,6 +29,7 @@ import {
buildShowExpiredExceptionsFilter,
getSavedObjectTypes,
} from '@kbn/securitysolution-list-utils';
+import { useEndpointExceptionsCapability } from '../../../../exceptions/hooks/use_endpoint_exceptions_capability';
import { useUserData } from '../../../../detections/components/user_info';
import { useKibana, useToasts } from '../../../../common/lib/kibana';
import { ExceptionsViewerSearchBar } from './search_bar';
@@ -120,6 +121,8 @@ const ExceptionsViewerComponent = ({
[listTypes]
);
+ const canWriteEndpointExceptions = useEndpointExceptionsCapability('crudEndpointExceptions');
+
// Reducer state
const [
{
@@ -531,7 +534,7 @@ const ExceptionsViewerComponent = ({
);
- const popoverTitle = getCapitalizedStatusText(RuleExecutionStatus['partial failure']);
+ const popoverTitle = getCapitalizedStatusText(RuleExecutionStatusEnum['partial failure']);
return (
);
- const popoverTitle = getCapitalizedStatusText(RuleExecutionStatus['partial failure']);
+ const popoverTitle = getCapitalizedStatusText(RuleExecutionStatusEnum['partial failure']);
return (
theme.eui.euiSizeXS};
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_exception_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_exception_actions.tsx
index 012b0dc65b82f..b2039befafd8a 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_exception_actions.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_exception_actions.tsx
@@ -8,8 +8,7 @@
import { useCallback, useMemo } from 'react';
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
-import { useListsConfig } from '../../../containers/detection_engine/lists/use_lists_config';
-import { useHasSecurityCapability } from '../../../../helper_hooks';
+import { useEndpointExceptionsCapability } from '../../../../exceptions/hooks/use_endpoint_exceptions_capability';
import { useUserData } from '../../user_info';
import { ACTION_ADD_ENDPOINT_EXCEPTION, ACTION_ADD_EXCEPTION } from '../translations';
import type { AlertTableContextMenuItem } from '../types';
@@ -76,14 +75,7 @@ export const useAlertExceptionActions = ({
onAddExceptionTypeClick,
});
- const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } =
- useListsConfig();
- const canReadEndpointExceptions = useHasSecurityCapability('crudEndpointExceptions');
-
- const canWriteEndpointExceptions = useMemo(
- () => !listsConfigLoading && !needsListsConfiguration && canReadEndpointExceptions,
- [canReadEndpointExceptions, listsConfigLoading, needsListsConfiguration]
- );
+ const canWriteEndpointExceptions = useEndpointExceptionsCapability('crudEndpointExceptions');
// Endpoint exceptions are available for:
// Serverless Endpoint Essentials/Complete PLI and
// on ESS Security Kibana sub-feature Endpoint Exceptions (enabled when Security feature is enabled)
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.test.tsx
index 7d202129a0013..5d31247e1f6ff 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.test.tsx
@@ -8,12 +8,12 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
-import { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring';
import { RuleStatusBadge } from './rule_status_badge';
describe('RuleStatusBadge', () => {
it('renders capitalized status text', () => {
- render();
+ render();
expect(screen.getByText('Succeeded')).toBeInTheDocument();
});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.tsx
index 89d1a63305faa..d52190839c37f 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.tsx
@@ -11,7 +11,8 @@ import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { HealthTruncateText } from '../../../../common/components/health_truncate_text';
import { getCapitalizedStatusText, getStatusColor } from './utils';
-import { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring';
+import type { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring';
interface RuleStatusBadgeProps {
status: RuleExecutionStatus | null | undefined;
@@ -29,7 +30,8 @@ const RuleStatusBadgeComponent = ({
showTooltip = true,
}: RuleStatusBadgeProps) => {
const isFailedStatus =
- status === RuleExecutionStatus.failed || status === RuleExecutionStatus['partial failure'];
+ status === RuleExecutionStatusEnum.failed ||
+ status === RuleExecutionStatusEnum['partial failure'];
const statusText = getCapitalizedStatusText(status);
const statusTooltip = isFailedStatus && message ? message : statusText;
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx
index 68927bf64faa3..4902ac0115fee 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx
@@ -8,7 +8,8 @@
import React from 'react';
import { render } from '@testing-library/react';
-import { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring';
+import type { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring';
import { RuleStatusFailedCallOut } from './rule_status_failed_callout';
jest.mock('../../../../common/lib/kibana');
@@ -32,22 +33,22 @@ describe('RuleStatusFailedCallOut', () => {
});
it('is hidden if status is "going to run"', () => {
- const result = renderWith(RuleExecutionStatus['going to run']);
+ const result = renderWith(RuleExecutionStatusEnum['going to run']);
expect(result.queryByTestId(TEST_ID)).toBe(null);
});
it('is hidden if status is "running"', () => {
- const result = renderWith(RuleExecutionStatus.running);
+ const result = renderWith(RuleExecutionStatusEnum.running);
expect(result.queryByTestId(TEST_ID)).toBe(null);
});
it('is hidden if status is "succeeded"', () => {
- const result = renderWith(RuleExecutionStatus.succeeded);
+ const result = renderWith(RuleExecutionStatusEnum.succeeded);
expect(result.queryByTestId(TEST_ID)).toBe(null);
});
it('is visible if status is "partial failure"', () => {
- const result = renderWith(RuleExecutionStatus['partial failure']);
+ const result = renderWith(RuleExecutionStatusEnum['partial failure']);
result.getByTestId(TEST_ID);
result.getByText('Warning at');
result.getByText('Jan 27, 2022 @ 15:03:31.176');
@@ -55,7 +56,7 @@ describe('RuleStatusFailedCallOut', () => {
});
it('is visible if status is "failed"', () => {
- const result = renderWith(RuleExecutionStatus.failed);
+ const result = renderWith(RuleExecutionStatusEnum.failed);
result.getByTestId(TEST_ID);
result.getByText('Rule failure at');
result.getByText('Jan 27, 2022 @ 15:03:31.176');
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx
index 83cd1e650c150..b9e26dc162207 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx
@@ -10,7 +10,8 @@ import React from 'react';
import { EuiCallOut, EuiCodeBlock } from '@elastic/eui';
import { FormattedDate } from '../../../../common/components/formatted_date';
-import { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring';
+import type { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring';
import * as i18n from './translations';
@@ -75,13 +76,13 @@ interface HelperProps {
const getPropsByStatus = (status: RuleExecutionStatus | null | undefined): HelperProps => {
switch (status) {
- case RuleExecutionStatus.failed:
+ case RuleExecutionStatusEnum.failed:
return {
shouldBeDisplayed: true,
color: 'danger',
title: i18n.ERROR_CALLOUT_TITLE,
};
- case RuleExecutionStatus['partial failure']:
+ case RuleExecutionStatusEnum['partial failure']:
return {
shouldBeDisplayed: true,
color: 'warning',
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.ts
index 899323c9faa2f..c13af24b63c25 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.ts
@@ -8,13 +8,14 @@
import type { IconColor } from '@elastic/eui';
import { capitalize } from 'lodash';
import { assertUnreachable } from '../../../../../common/utility_types';
-import { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring';
+import type { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring';
export const getStatusText = (value: RuleExecutionStatus | null | undefined): string | null => {
if (value == null) {
return null;
}
- if (value === RuleExecutionStatus['partial failure']) {
+ if (value === RuleExecutionStatusEnum['partial failure']) {
return 'warning';
}
return value;
@@ -31,16 +32,16 @@ export const getStatusColor = (status: RuleExecutionStatus | null | undefined):
if (status == null) {
return 'subdued';
}
- if (status === RuleExecutionStatus.succeeded) {
+ if (status === RuleExecutionStatusEnum.succeeded) {
return 'success';
}
- if (status === RuleExecutionStatus.failed) {
+ if (status === RuleExecutionStatusEnum.failed) {
return 'danger';
}
if (
- status === RuleExecutionStatus.running ||
- status === RuleExecutionStatus['partial failure'] ||
- status === RuleExecutionStatus['going to run']
+ status === RuleExecutionStatusEnum.running ||
+ status === RuleExecutionStatusEnum['partial failure'] ||
+ status === RuleExecutionStatusEnum['going to run']
) {
return 'warning';
}
diff --git a/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx b/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx
index 1b1b6ad36d1ea..990fd818505b7 100644
--- a/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx
+++ b/x-pack/plugins/security_solution/public/exceptions/components/list_exception_items/index.tsx
@@ -4,8 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import React, { useMemo } from 'react';
import type { FC } from 'react';
+import React, { useMemo } from 'react';
import type {
ExceptionListItemIdentifiers,
GetExceptionItemProps,
@@ -13,8 +13,8 @@ import type {
ViewerStatus,
} from '@kbn/securitysolution-exception-list-components';
import { ExceptionItems } from '@kbn/securitysolution-exception-list-components';
-import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
+import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import type { Pagination } from '@elastic/eui';
import { FormattedDate } from '../../../common/components/formatted_date';
@@ -22,6 +22,7 @@ import { getFormattedComments } from '../../utils/ui.helpers';
import { LinkToRuleDetails } from '../link_to_rule_details';
import { ExceptionsUtility } from '../exceptions_utility';
import * as i18n from '../../translations/list_exception_items';
+import { useEndpointExceptionsCapability } from '../../hooks/use_endpoint_exceptions_capability';
interface ListExceptionItemsProps {
isReadOnly: boolean;
@@ -58,6 +59,8 @@ const ListExceptionItemsComponent: FC = ({
onPaginationChange,
onCreateExceptionListItem,
}) => {
+ const canWriteEndpointExceptions = useEndpointExceptionsCapability('crudEndpointExceptions');
+
const editButtonText = useMemo(() => {
return listType === ExceptionListTypeEnum.ENDPOINT
? i18n.EXCEPTION_ITEM_CARD_EDIT_ENDPOINT_LABEL
@@ -76,7 +79,7 @@ const ListExceptionItemsComponent: FC = ({
viewerStatus={viewerStatus as ViewerStatus}
listType={listType as ExceptionListTypeEnum}
ruleReferences={ruleReferences}
- isReadOnly={isReadOnly}
+ isReadOnly={isReadOnly || !canWriteEndpointExceptions}
exceptions={exceptions}
emptyViewerTitle={emptyViewerTitle}
emptyViewerBody={emptyViewerBody}
diff --git a/x-pack/plugins/security_solution/public/exceptions/hooks/use_endpoint_exceptions_capability/index.tsx b/x-pack/plugins/security_solution/public/exceptions/hooks/use_endpoint_exceptions_capability/index.tsx
new file mode 100644
index 0000000000000..a96b11225e659
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/exceptions/hooks/use_endpoint_exceptions_capability/index.tsx
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useMemo } from 'react';
+import { useListsConfig } from '../../../detections/containers/detection_engine/lists/use_lists_config';
+import { useHasSecurityCapability } from '../../../helper_hooks';
+
+export const useEndpointExceptionsCapability = (
+ capability: 'showEndpointExceptions' | 'crudEndpointExceptions'
+) => {
+ const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } =
+ useListsConfig();
+ const hasEndpointExceptionCapability = useHasSecurityCapability(capability);
+
+ return useMemo(
+ () => !listsConfigLoading && !needsListsConfiguration && hasEndpointExceptionCapability,
+ [hasEndpointExceptionCapability, listsConfigLoading, needsListsConfiguration]
+ );
+};
diff --git a/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx b/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx
index 0e457d520984c..3c0fab0343a36 100644
--- a/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx
+++ b/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx
@@ -28,7 +28,6 @@ import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { useApi, useExceptionLists } from '@kbn/securitysolution-list-hooks';
import { EmptyViewerState, ViewerStatus } from '@kbn/securitysolution-exception-list-components';
-import { useHasSecurityCapability } from '../../../helper_hooks';
import { AutoDownload } from '../../../common/components/auto_download/auto_download';
import { useKibana } from '../../../common/lib/kibana';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
@@ -52,6 +51,7 @@ import { MissingPrivilegesCallOut } from '../../../detections/components/callout
import { ALL_ENDPOINT_ARTIFACT_LIST_IDS } from '../../../../common/endpoint/service/artifacts/constants';
import { AddExceptionFlyout } from '../../../detection_engine/rule_exceptions/components/add_exception_flyout';
+import { useEndpointExceptionsCapability } from '../../hooks/use_endpoint_exceptions_capability';
export type Func = () => Promise;
@@ -82,15 +82,10 @@ const SORT_FIELDS: Array<{ field: string; label: string; defaultOrder: 'asc' | '
export const SharedLists = React.memo(() => {
const [{ loading: userInfoLoading, canUserCRUD, canUserREAD }] = useUserData();
- const { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration } =
- useListsConfig();
+ const { loading: listsConfigLoading } = useListsConfig();
const loading = userInfoLoading || listsConfigLoading;
- const canShowEndpointExceptions = useHasSecurityCapability('showEndpointExceptions');
- const canAccessEndpointExceptions = useMemo(
- () => !listsConfigLoading && !needsListsConfiguration && canShowEndpointExceptions,
- [canShowEndpointExceptions, listsConfigLoading, needsListsConfiguration]
- );
+ const canAccessEndpointExceptions = useEndpointExceptionsCapability('showEndpointExceptions');
const {
services: {
http,
diff --git a/x-pack/plugins/security_solution/public/exceptions/routes.tsx b/x-pack/plugins/security_solution/public/exceptions/routes.tsx
index 747a96cb72eea..e6faf794dcdbf 100644
--- a/x-pack/plugins/security_solution/public/exceptions/routes.tsx
+++ b/x-pack/plugins/security_solution/public/exceptions/routes.tsx
@@ -5,21 +5,22 @@
* 2.0.
*/
import React from 'react';
-import { Routes, Route } from '@kbn/shared-ux-router';
+import { Route, Routes } from '@kbn/shared-ux-router';
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
import * as i18n from './translations';
import {
+ EXCEPTION_LIST_DETAIL_PATH,
EXCEPTIONS_PATH,
SecurityPageName,
- EXCEPTION_LIST_DETAIL_PATH,
} from '../../common/constants';
-import { SharedLists, ListsDetailView } from './pages';
+import { ListsDetailView, SharedLists } from './pages';
import { SpyRoute } from '../common/utils/route/spy_routes';
import { NotFoundPage } from '../app/404';
import { useReadonlyHeader } from '../use_readonly_header';
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
+import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper';
const ExceptionsRoutes = () => (
@@ -32,9 +33,9 @@ const ExceptionsRoutes = () => (
const ExceptionsListDetailRoute = () => (
-
+
-
+
);
diff --git a/x-pack/plugins/security_solution/public/helpers.tsx b/x-pack/plugins/security_solution/public/helpers.tsx
index 0dfc0878631cc..b245a6ae2f6bc 100644
--- a/x-pack/plugins/security_solution/public/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/helpers.tsx
@@ -21,6 +21,7 @@ import {
APP_UI_ID,
CASES_FEATURE_ID,
CASES_PATH,
+ DASHBOARDS_PATH,
EXCEPTIONS_PATH,
RULES_PATH,
SERVER_APP_ID,
@@ -173,6 +174,13 @@ export const isDetectionsPath = (pathname: string): boolean => {
});
};
+export const isDashboardViewPath = (pathname: string): boolean =>
+ matchPath(pathname, {
+ path: `/${DASHBOARDS_PATH}/:id`,
+ exact: false,
+ strict: false,
+ }) != null;
+
const isAlertsPath = (pathname: string): boolean => {
return !!matchPath(pathname, {
path: `${ALERTS_PATH}`,
diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx
index 904ec870e9a2c..65453e37d686b 100644
--- a/x-pack/plugins/security_solution/public/plugin.tsx
+++ b/x-pack/plugins/security_solution/public/plugin.tsx
@@ -187,6 +187,7 @@ export class Plugin implements IPlugin void;
/**
diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
index 917d366354270..3fb28c1c5099d 100644
--- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts
@@ -6,27 +6,27 @@
*/
import type {
+ ElasticsearchClient,
KibanaRequest,
Logger,
- ElasticsearchClient,
SavedObjectsClientContract,
} from '@kbn/core/server';
import type { ExceptionListClient, ListsServerExtensionRegistrar } from '@kbn/lists-plugin/server';
import type { CasesClient, CasesStart } from '@kbn/cases-plugin/server';
import type { SecurityPluginStart } from '@kbn/security-plugin/server';
import type {
+ FleetFromHostFileClientInterface,
FleetStartContract,
MessageSigningServiceInterface,
- FleetFromHostFileClientInterface,
} from '@kbn/fleet-plugin/server';
import type { PluginStartContract as AlertsPluginStartContract } from '@kbn/alerting-plugin/server';
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { FleetActionsClientInterface } from '@kbn/fleet-plugin/server/services/actions/types';
import {
getPackagePolicyCreateCallback,
- getPackagePolicyUpdateCallback,
getPackagePolicyDeleteCallback,
getPackagePolicyPostCreateCallback,
+ getPackagePolicyUpdateCallback,
} from '../fleet_integration/fleet_integration';
import type { ManifestManager } from './services/artifacts';
import type { ConfigType } from '../config';
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/service.ts b/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/service.ts
index f736fffffcf17..ffebaac4420fc 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/service.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/feature_usage/service.ts
@@ -24,6 +24,7 @@ const FEATURES = {
GET_FILE: 'Get file',
EXECUTE: 'Execute command',
ALERTS_BY_PROCESS_ANCESTRY: 'Get related alerts by process ancestry',
+ ENDPOINT_EXCEPTIONS: 'Endpoint exceptions',
} as const;
export type FeatureKeys = keyof typeof FEATURES;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts
index 92f610391753e..546256889ecfb 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts
@@ -8,7 +8,6 @@
import type { RulesClient } from '@kbn/alerting-plugin/server';
import type { ExceptionListClient } from '@kbn/lists-plugin/server';
import { transformError } from '@kbn/securitysolution-es-utils';
-import { validate } from '@kbn/securitysolution-io-ts-utils';
import moment from 'moment';
import {
InstallPrebuiltRulesAndTimelinesResponse,
@@ -116,9 +115,7 @@ export const createPrepackagedRules = async (
throw new AggregateError(result.errors, 'Error installing new prebuilt rules');
}
- const { result: timelinesResult, error: timelinesError } = await performTimelinesInstallation(
- context
- );
+ const { result: timelinesResult } = await performTimelinesInstallation(context);
await upgradePrebuiltRules(rulesClient, rulesToUpdate);
@@ -129,17 +126,5 @@ export const createPrepackagedRules = async (
timelines_updated: timelinesResult?.timelines_updated ?? 0,
};
- const [validated, genericErrors] = validate(
- prebuiltRulesOutput,
- InstallPrebuiltRulesAndTimelinesResponse
- );
-
- if (genericErrors != null && timelinesError != null) {
- throw new PrepackagedRulesError(
- [genericErrors, timelinesError].filter((msg) => msg != null).join(', '),
- 500
- );
- }
-
- return validated;
+ return InstallPrebuiltRulesAndTimelinesResponse.parse(prebuiltRulesOutput);
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/export_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/export_rules/route.ts
index 674b8433d5c00..97aaff3373e01 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/export_rules/route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/export_rules/route.ts
@@ -14,7 +14,7 @@ import {
ExportRulesRequestQuery,
} from '../../../../../../../common/api/detection_engine/rule_management';
-import { buildRouteValidation } from '../../../../../../utils/build_validation/route_validation';
+import { buildRouteValidationWithZod } from '../../../../../../utils/build_validation/route_validation';
import type { SecuritySolutionPluginRouter } from '../../../../../../types';
import type { ConfigType } from '../../../../../../config';
import { getNonPackagedRulesCount } from '../../../logic/search/get_existing_prepackaged_rules';
@@ -40,8 +40,8 @@ export const exportRulesRoute = (
version: '2023-10-31',
validate: {
request: {
- query: buildRouteValidation(ExportRulesRequestQuery),
- body: buildRouteValidation(ExportRulesRequestBody),
+ query: buildRouteValidationWithZod(ExportRulesRequestQuery),
+ body: buildRouteValidationWithZod(ExportRulesRequestBody),
},
},
},
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts
index 3ec268676801d..eb622884bc7a9 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts
@@ -5,40 +5,34 @@
* 2.0.
*/
-import { chunk } from 'lodash/fp';
-import { extname } from 'path';
import { schema } from '@kbn/config-schema';
-import { createPromiseFromStreams } from '@kbn/utils';
-
-import { transformError } from '@kbn/securitysolution-es-utils';
-import { validate } from '@kbn/securitysolution-io-ts-utils';
-
import type { IKibanaResponse } from '@kbn/core/server';
-import { DETECTION_ENGINE_RULES_URL } from '../../../../../../../common/constants';
-import type { ImportRulesRequestQueryDecoded } from '../../../../../../../common/api/detection_engine/rule_management';
+import { transformError } from '@kbn/securitysolution-es-utils';
+import { createPromiseFromStreams } from '@kbn/utils';
+import { chunk } from 'lodash/fp';
+import { extname } from 'path';
import {
ImportRulesRequestQuery,
ImportRulesResponse,
} from '../../../../../../../common/api/detection_engine/rule_management';
-
-import type { HapiReadableStream, SecuritySolutionPluginRouter } from '../../../../../../types';
+import { DETECTION_ENGINE_RULES_URL } from '../../../../../../../common/constants';
import type { ConfigType } from '../../../../../../config';
import type { SetupPlugins } from '../../../../../../plugin';
+import type { HapiReadableStream, SecuritySolutionPluginRouter } from '../../../../../../types';
+import { buildRouteValidationWithZod } from '../../../../../../utils/build_validation/route_validation';
import { buildMlAuthz } from '../../../../../machine_learning/authz';
-import type { ImportRuleResponse, BulkError } from '../../../../routes/utils';
-import { isBulkError, isImportRegular, buildSiemResponse } from '../../../../routes/utils';
-
-import {
- getTupleDuplicateErrorsAndUniqueRules,
- migrateLegacyActionsIds,
-} from '../../../utils/utils';
+import type { BulkError, ImportRuleResponse } from '../../../../routes/utils';
+import { buildSiemResponse, isBulkError, isImportRegular } from '../../../../routes/utils';
+import { importRuleActionConnectors } from '../../../logic/import/action_connectors/import_rule_action_connectors';
import { createRulesAndExceptionsStreamFromNdJson } from '../../../logic/import/create_rules_stream_from_ndjson';
-import { buildRouteValidation } from '../../../../../../utils/build_validation/route_validation';
+import { getReferencedExceptionLists } from '../../../logic/import/gather_referenced_exceptions';
import type { RuleExceptionsPromiseFromStreams } from '../../../logic/import/import_rules_utils';
import { importRules as importRulesHelper } from '../../../logic/import/import_rules_utils';
-import { getReferencedExceptionLists } from '../../../logic/import/gather_referenced_exceptions';
import { importRuleExceptions } from '../../../logic/import/import_rule_exceptions';
-import { importRuleActionConnectors } from '../../../logic/import/action_connectors/import_rule_action_connectors';
+import {
+ getTupleDuplicateErrorsAndUniqueRules,
+ migrateLegacyActionsIds,
+} from '../../../utils/utils';
const CHUNK_PARSED_OBJECT_SIZE = 50;
@@ -64,10 +58,7 @@ export const importRulesRoute = (
version: '2023-10-31',
validate: {
request: {
- query: buildRouteValidation<
- typeof ImportRulesRequestQuery,
- ImportRulesRequestQueryDecoded
- >(ImportRulesRequestQuery),
+ query: buildRouteValidationWithZod(ImportRulesRequestQuery),
body: schema.any(), // validation on file object is accomplished later in the handler.
},
},
@@ -202,12 +193,7 @@ export const importRulesRoute = (
action_connectors_warnings: actionConnectorWarnings,
};
- const [validated, errors] = validate(importRules, ImportRulesResponse);
- if (errors != null) {
- return siemResponse.error({ statusCode: 500, body: errors });
- } else {
- return response.ok({ body: validated ?? {} });
- }
+ return response.ok({ body: ImportRulesResponse.parse(importRules) });
} catch (err) {
const error = transformError(err);
return siemResponse.error({
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/route.ts
index 12eb6dbd309a8..5120603f9f674 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/route.ts
@@ -29,11 +29,8 @@ export const readTagsRoute = (router: SecuritySolutionPluginRouter) => {
},
async (context, request, response): Promise> => {
const siemResponse = buildSiemResponse(response);
- const rulesClient = (await context.alerting)?.getRulesClient();
-
- if (!rulesClient) {
- return siemResponse.error({ statusCode: 404 });
- }
+ const ctx = await context.resolve(['alerting']);
+ const rulesClient = ctx.alerting.getRulesClient();
try {
const tags = await readTags({
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.ts
index 24b8949120cf5..e1981cf9a3937 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.ts
@@ -5,17 +5,17 @@
* 2.0.
*/
-import { transformError } from '@kbn/securitysolution-es-utils';
import type { IKibanaResponse } from '@kbn/core/server';
-import { buildRouteValidation } from '../../../../../../utils/build_validation/route_validation';
-import { buildSiemResponse } from '../../../../routes/utils';
+import { transformError } from '@kbn/securitysolution-es-utils';
import type { SecuritySolutionPluginRouter } from '../../../../../../types';
+import { buildRouteValidationWithZod } from '../../../../../../utils/build_validation/route_validation';
+import { buildSiemResponse } from '../../../../routes/utils';
import type { GetRuleExecutionResultsResponse } from '../../../../../../../common/api/detection_engine/rule_monitoring';
import {
- GET_RULE_EXECUTION_RESULTS_URL,
GetRuleExecutionResultsRequestParams,
GetRuleExecutionResultsRequestQuery,
+ GET_RULE_EXECUTION_RESULTS_URL,
} from '../../../../../../../common/api/detection_engine/rule_monitoring';
/**
@@ -36,8 +36,8 @@ export const getRuleExecutionResultsRoute = (router: SecuritySolutionPluginRoute
version: '1',
validate: {
request: {
- params: buildRouteValidation(GetRuleExecutionResultsRequestParams),
- query: buildRouteValidation(GetRuleExecutionResultsRequestQuery),
+ params: buildRouteValidationWithZod(GetRuleExecutionResultsRequestParams),
+ query: buildRouteValidationWithZod(GetRuleExecutionResultsRequestQuery),
},
},
},
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/event_log/aggregations/rule_execution_stats.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/event_log/aggregations/rule_execution_stats.ts
index fe5da1322fc45..7c3b595be1565 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/event_log/aggregations/rule_execution_stats.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/event_log/aggregations/rule_execution_stats.ts
@@ -15,10 +15,11 @@ import type {
NumberOfLoggedMessages,
RuleExecutionStats,
TopMessages,
+ RuleExecutionStatus,
} from '../../../../../../../../common/api/detection_engine/rule_monitoring';
import {
RuleExecutionEventType,
- RuleExecutionStatus,
+ RuleExecutionStatusEnum,
LogLevel,
} from '../../../../../../../../common/api/detection_engine/rule_monitoring';
@@ -72,8 +73,8 @@ export const getRuleExecutionStatsAggregation = (
{
terms: {
[f.RULE_EXECUTION_STATUS]: [
- RuleExecutionStatus.running,
- RuleExecutionStatus['going to run'],
+ RuleExecutionStatusEnum.running,
+ RuleExecutionStatusEnum['going to run'],
],
},
},
@@ -223,9 +224,9 @@ const normalizeNumberOfExecutions = (
return {
total: Number(totalExecutions.value || 0),
by_outcome: {
- succeeded: getStatusCount(RuleExecutionStatus.succeeded),
- warning: getStatusCount(RuleExecutionStatus['partial failure']),
- failed: getStatusCount(RuleExecutionStatus.failed),
+ succeeded: getStatusCount(RuleExecutionStatusEnum.succeeded),
+ warning: getStatusCount(RuleExecutionStatusEnum['partial failure']),
+ failed: getStatusCount(RuleExecutionStatusEnum.failed),
},
};
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts
index 2d14f62156db8..84cdc041f9a88 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts
@@ -5,30 +5,31 @@
* 2.0.
*/
+import type { Logger } from '@kbn/core/server';
import { sum } from 'lodash';
import type { Duration } from 'moment';
-import type { Logger } from '@kbn/core/server';
import type {
- PublicRuleResultService,
PublicRuleMonitoringService,
+ PublicRuleResultService,
} from '@kbn/alerting-plugin/server/types';
import type {
RuleExecutionMetrics,
RuleExecutionSettings,
+ RuleExecutionStatus,
} from '../../../../../../../common/api/detection_engine/rule_monitoring';
import {
LogLevel,
logLevelFromExecutionStatus,
LogLevelSetting,
logLevelToNumber,
- RuleExecutionStatus,
+ RuleExecutionStatusEnum,
} from '../../../../../../../common/api/detection_engine/rule_monitoring';
import { assertUnreachable } from '../../../../../../../common/utility_types';
import { withSecuritySpan } from '../../../../../../utils/with_security_span';
-import { truncateValue } from '../../utils/normalization';
import type { ExtMeta } from '../../utils/console_logging';
+import { truncateValue } from '../../utils/normalization';
import { getCorrelationIds } from './correlation_ids';
import type { IEventLogWriter } from '../event_log/event_log_writer';
@@ -164,7 +165,7 @@ export const createRuleExecutionLogClientForExecutors = (
const writeStatusChangeToRuleObject = async (args: NormalizedStatusChangeArgs): Promise => {
const { newStatus, message, metrics } = args;
- if (newStatus === RuleExecutionStatus.running) {
+ if (newStatus === RuleExecutionStatusEnum.running) {
return;
}
@@ -186,9 +187,9 @@ export const createRuleExecutionLogClientForExecutors = (
ruleMonitoringService.setLastRunMetricsGapDurationS(executionGapDurationS);
}
- if (newStatus === RuleExecutionStatus.failed) {
+ if (newStatus === RuleExecutionStatusEnum.failed) {
ruleResultService.addLastRunError(message);
- } else if (newStatus === RuleExecutionStatus['partial failure']) {
+ } else if (newStatus === RuleExecutionStatusEnum['partial failure']) {
ruleResultService.addLastRunWarning(message);
}
@@ -234,7 +235,7 @@ interface NormalizedStatusChangeArgs {
}
const normalizeStatusChangeArgs = (args: StatusChangeArgs): NormalizedStatusChangeArgs => {
- if (args.newStatus === RuleExecutionStatus.running) {
+ if (args.newStatus === RuleExecutionStatusEnum.running) {
return {
newStatus: args.newStatus,
message: '',
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts
index e1d0998a21225..c6a133dce01d7 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts
@@ -6,7 +6,10 @@
*/
import type { Duration } from 'moment';
-import type { RuleExecutionStatus } from '../../../../../../../common/api/detection_engine/rule_monitoring';
+import type {
+ RuleExecutionStatus,
+ RuleExecutionStatusEnum,
+} from '../../../../../../../common/api/detection_engine/rule_monitoring';
/**
* Used from rule executors to log various information about the rule execution:
@@ -109,7 +112,7 @@ export interface RuleExecutionContext {
}
export interface RunningStatusChangeArgs {
- newStatus: RuleExecutionStatus.running;
+ newStatus: RuleExecutionStatusEnum['running'];
}
/**
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/create_rule_execution_summary.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/create_rule_execution_summary.ts
index 7dbac0ded96e5..f72b110a844b3 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/create_rule_execution_summary.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/create_rule_execution_summary.ts
@@ -9,9 +9,9 @@ import type { ResolvedSanitizedRule, SanitizedRule } from '@kbn/alerting-plugin/
import type { RuleExecutionSummary } from '../../../../../../common/api/detection_engine/rule_monitoring';
import {
- ruleLastRunOutcomeToExecutionStatus,
+ RuleExecutionStatusEnum,
ruleExecutionStatusToNumber,
- RuleExecutionStatus,
+ ruleLastRunOutcomeToExecutionStatus,
} from '../../../../../../common/api/detection_engine/rule_monitoring';
import type { RuleParams } from '../../../rule_schema';
@@ -39,8 +39,8 @@ export const createRuleExecutionSummary = (
return {
last_execution: {
date: lastRunInternal.timestamp,
- status: RuleExecutionStatus.running,
- status_order: ruleExecutionStatusToNumber(RuleExecutionStatus.running),
+ status: RuleExecutionStatusEnum.running,
+ status_order: ruleExecutionStatusToNumber(RuleExecutionStatusEnum.running),
message: '',
metrics: {},
},
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/aggregations/execution_results/index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/aggregations/execution_results/index.test.ts
index 234eac9d594e6..79ce2503cbe9b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/aggregations/execution_results/index.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/aggregations/execution_results/index.test.ts
@@ -13,7 +13,7 @@
*/
import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules';
-import { RuleExecutionStatus } from '../../../../../../../../../common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '../../../../../../../../../common/api/detection_engine/rule_monitoring';
import {
formatExecutionEventResponse,
@@ -1328,30 +1328,30 @@ describe('mapRuleStatusToPlatformStatus', () => {
expect(mapRuleExecutionStatusToPlatformStatus([])).toEqual([]);
});
- test('should correctly translate RuleExecutionStatus.failed to `failure` platform status', () => {
- expect(mapRuleExecutionStatusToPlatformStatus([RuleExecutionStatus.failed])).toEqual([
+ test('should correctly translate RuleExecutionStatusEnum.failed to `failure` platform status', () => {
+ expect(mapRuleExecutionStatusToPlatformStatus([RuleExecutionStatusEnum.failed])).toEqual([
'failure',
]);
});
- test('should correctly translate RuleExecutionStatus.succeeded to `success` platform status', () => {
- expect(mapRuleExecutionStatusToPlatformStatus([RuleExecutionStatus.succeeded])).toEqual([
+ test('should correctly translate RuleExecutionStatusEnum.succeeded to `success` platform status', () => {
+ expect(mapRuleExecutionStatusToPlatformStatus([RuleExecutionStatusEnum.succeeded])).toEqual([
'success',
]);
});
- test('should correctly translate RuleExecutionStatus.["going to run"] to empty array platform status', () => {
- expect(mapRuleExecutionStatusToPlatformStatus([RuleExecutionStatus['going to run']])).toEqual(
- []
- );
+ test('should correctly translate RuleExecutionStatusEnum.["going to run"] to empty array platform status', () => {
+ expect(
+ mapRuleExecutionStatusToPlatformStatus([RuleExecutionStatusEnum['going to run']])
+ ).toEqual([]);
});
test("should correctly translate multiple RuleExecutionStatus's to platform statuses", () => {
expect(
mapRuleExecutionStatusToPlatformStatus([
- RuleExecutionStatus.succeeded,
- RuleExecutionStatus.failed,
- RuleExecutionStatus['going to run'],
+ RuleExecutionStatusEnum.succeeded,
+ RuleExecutionStatusEnum.failed,
+ RuleExecutionStatusEnum['going to run'],
]).sort()
).toEqual(['failure', 'success']);
});
@@ -1362,13 +1362,15 @@ describe('mapPlatformStatusToRuleExecutionStatus', () => {
expect(mapPlatformStatusToRuleExecutionStatus('')).toEqual(undefined);
});
- test('should correctly translate `failure` platform status to `RuleExecutionStatus.failed`', () => {
- expect(mapPlatformStatusToRuleExecutionStatus('failure')).toEqual(RuleExecutionStatus.failed);
+ test('should correctly translate `failure` platform status to `RuleExecutionStatusEnum.failed`', () => {
+ expect(mapPlatformStatusToRuleExecutionStatus('failure')).toEqual(
+ RuleExecutionStatusEnum.failed
+ );
});
- test('should correctly translate `success` platform status to `RuleExecutionStatus.succeeded`', () => {
+ test('should correctly translate `success` platform status to `RuleExecutionStatusEnum.succeeded`', () => {
expect(mapPlatformStatusToRuleExecutionStatus('success')).toEqual(
- RuleExecutionStatus.succeeded
+ RuleExecutionStatusEnum.succeeded
);
});
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/aggregations/execution_results/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/aggregations/execution_results/index.ts
index d2077208e718e..e4e394fd16149 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/aggregations/execution_results/index.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/aggregations/execution_results/index.ts
@@ -5,23 +5,24 @@
* 2.0.
*/
-import { flatMap, get } from 'lodash';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import type { AggregateEventsBySavedObjectResult } from '@kbn/event-log-plugin/server';
import { BadRequestError } from '@kbn/securitysolution-es-utils';
import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules';
-import type { AggregateEventsBySavedObjectResult } from '@kbn/event-log-plugin/server';
+import { flatMap, get } from 'lodash';
import type {
- RuleExecutionResult,
GetRuleExecutionResultsResponse,
+ RuleExecutionResult,
+ RuleExecutionStatus,
} from '../../../../../../../../../common/api/detection_engine/rule_monitoring';
-import { RuleExecutionStatus } from '../../../../../../../../../common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '../../../../../../../../../common/api/detection_engine/rule_monitoring';
+import * as f from '../../../../event_log/event_log_fields';
import type {
ExecutionEventAggregationOptions,
- ExecutionUuidAggResult,
ExecutionUuidAggBucket,
+ ExecutionUuidAggResult,
} from './types';
-import * as f from '../../../../event_log/event_log_fields';
// TODO: https://github.com/elastic/kibana/issues/125642 Move the fields from this file to `event_log_fields.ts`
@@ -378,9 +379,9 @@ export const mapRuleExecutionStatusToPlatformStatus = (
): string[] => {
return flatMap(ruleStatuses, (rs) => {
switch (rs) {
- case RuleExecutionStatus.failed:
+ case RuleExecutionStatusEnum.failed:
return 'failure';
- case RuleExecutionStatus.succeeded:
+ case RuleExecutionStatusEnum.succeeded:
return 'success';
default:
return [];
@@ -397,9 +398,9 @@ export const mapPlatformStatusToRuleExecutionStatus = (
): RuleExecutionStatus | undefined => {
switch (platformStatus) {
case 'failure':
- return RuleExecutionStatus.failed;
+ return RuleExecutionStatusEnum.failed;
case 'success':
- return RuleExecutionStatus.succeeded;
+ return RuleExecutionStatusEnum.succeeded;
default:
return undefined;
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts
index 6a30227b82f03..b41edf380fcfa 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts
@@ -24,7 +24,7 @@ import {
DETECTION_ENGINE_RULES_PREVIEW,
} from '../../../../../../common/constants';
import { validateCreateRuleProps } from '../../../../../../common/api/detection_engine/rule_management';
-import { RuleExecutionStatus } from '../../../../../../common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '../../../../../../common/api/detection_engine/rule_monitoring';
import type {
PreviewResponse,
RulePreviewLogs,
@@ -287,11 +287,11 @@ export const previewRulesRoute = async (
})) as { state: TState });
const errors = loggedStatusChanges
- .filter((item) => item.newStatus === RuleExecutionStatus.failed)
+ .filter((item) => item.newStatus === RuleExecutionStatusEnum.failed)
.map((item) => item.message ?? 'Unknown Error');
const warnings = loggedStatusChanges
- .filter((item) => item.newStatus === RuleExecutionStatus['partial failure'])
+ .filter((item) => item.newStatus === RuleExecutionStatusEnum['partial failure'])
.map((item) => item.message ?? 'Unknown Warning');
logs.push({
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts
index a2cfc34b1d1d8..ec1db8812966e 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts
@@ -36,7 +36,7 @@ import { getNotificationResultsLink } from '../rule_actions_legacy';
import { formatAlertForNotificationActions } from '../rule_actions_legacy/logic/notifications/schedule_notification_actions';
import { createResultObject } from './utils';
import { bulkCreateFactory, wrapHitsFactory, wrapSequencesFactory } from './factories';
-import { RuleExecutionStatus } from '../../../../common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '../../../../common/api/detection_engine/rule_monitoring';
import { truncateList } from '../rule_monitoring';
import aadFieldConversion from '../routes/index/signal_aad_mapping.json';
import { extractReferences, injectReferences } from './saved_object_references';
@@ -179,7 +179,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
ruleExecutionLogger.debug(`Starting Security Rule execution (interval: ${interval})`);
await ruleExecutionLogger.logStatusChange({
- newStatus: RuleExecutionStatus.running,
+ newStatus: RuleExecutionStatusEnum.running,
});
let result = createResultObject(state);
@@ -240,7 +240,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
: `Check for indices to search failed ${exc}`;
await ruleExecutionLogger.logStatusChange({
- newStatus: RuleExecutionStatus.failed,
+ newStatus: RuleExecutionStatusEnum.failed,
message: errorMessage,
});
@@ -299,7 +299,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
}
} catch (exc) {
await ruleExecutionLogger.logStatusChange({
- newStatus: RuleExecutionStatus['partial failure'],
+ newStatus: RuleExecutionStatusEnum['partial failure'],
message: `Check privileges failed to execute ${exc}`,
});
wroteWarningStatus = true;
@@ -321,7 +321,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
const gapDuration = `${remainingGap.humanize()} (${remainingGap.asMilliseconds()}ms)`;
await ruleExecutionLogger.logStatusChange({
- newStatus: RuleExecutionStatus.failed,
+ newStatus: RuleExecutionStatusEnum.failed,
message: `${gapDuration} were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances`,
metrics: { executionGap: remainingGap },
});
@@ -459,7 +459,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
if (result.warningMessages.length) {
await ruleExecutionLogger.logStatusChange({
- newStatus: RuleExecutionStatus['partial failure'],
+ newStatus: RuleExecutionStatusEnum['partial failure'],
message: truncateList(result.warningMessages).join(),
metrics: {
searchDurations: result.searchAfterTimes,
@@ -485,7 +485,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
if (!hasError && !wroteWarningStatus && !result.warning) {
await ruleExecutionLogger.logStatusChange({
- newStatus: RuleExecutionStatus.succeeded,
+ newStatus: RuleExecutionStatusEnum.succeeded,
message: 'Rule execution completed successfully',
metrics: {
searchDurations: result.searchAfterTimes,
@@ -495,7 +495,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
});
} else if (wroteWarningStatus && !hasError && !result.warning) {
await ruleExecutionLogger.logStatusChange({
- newStatus: RuleExecutionStatus['partial failure'],
+ newStatus: RuleExecutionStatusEnum['partial failure'],
message: warningMessage,
metrics: {
searchDurations: result.searchAfterTimes,
@@ -506,7 +506,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
}
} else {
await ruleExecutionLogger.logStatusChange({
- newStatus: RuleExecutionStatus.failed,
+ newStatus: RuleExecutionStatusEnum.failed,
message: `Bulk Indexing of alerts failed: ${truncateList(result.errors).join()}`,
metrics: {
searchDurations: result.searchAfterTimes,
@@ -519,7 +519,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
const errorMessage = error.message ?? '(no error message given)';
await ruleExecutionLogger.logStatusChange({
- newStatus: RuleExecutionStatus.failed,
+ newStatus: RuleExecutionStatusEnum.failed,
message: `An error occurred during rule execution: message: "${errorMessage}"`,
metrics: {
searchDurations: result.searchAfterTimes,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.test.ts
index 0a18cd40b3a61..b2d93868cd976 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.test.ts
@@ -14,7 +14,7 @@ import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
import { listMock } from '@kbn/lists-plugin/server/mocks';
import type { ExceptionListClient } from '@kbn/lists-plugin/server';
-import { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring';
import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock';
import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock';
@@ -655,7 +655,7 @@ describe('utils', () => {
expect(wroteWarningStatus).toBeTruthy();
expect(foundNoIndices).toBeFalsy();
expect(ruleExecutionLogger.logStatusChange).toHaveBeenCalledWith({
- newStatus: RuleExecutionStatus['partial failure'],
+ newStatus: RuleExecutionStatusEnum['partial failure'],
message:
'The following indices are missing the timestamp override field "event.ingested": ["myfakeindex-1","myfakeindex-2"]',
});
@@ -699,7 +699,7 @@ describe('utils', () => {
expect(wroteWarningStatus).toBeTruthy();
expect(foundNoIndices).toBeFalsy();
expect(ruleExecutionLogger.logStatusChange).toHaveBeenCalledWith({
- newStatus: RuleExecutionStatus['partial failure'],
+ newStatus: RuleExecutionStatusEnum['partial failure'],
message:
'The following indices are missing the timestamp field "@timestamp": ["myfakeindex-1","myfakeindex-2"]',
});
@@ -732,7 +732,7 @@ describe('utils', () => {
expect(wroteWarningStatus).toBeTruthy();
expect(foundNoIndices).toBeTruthy();
expect(ruleExecutionLogger.logStatusChange).toHaveBeenCalledWith({
- newStatus: RuleExecutionStatus['partial failure'],
+ newStatus: RuleExecutionStatusEnum['partial failure'],
message:
'This rule is attempting to query data from Elasticsearch indices listed in the "Index patterns" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is disabled. If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent.',
});
@@ -766,7 +766,7 @@ describe('utils', () => {
expect(wroteWarningStatus).toBeTruthy();
expect(foundNoIndices).toBeTruthy();
expect(ruleExecutionLogger.logStatusChange).toHaveBeenCalledWith({
- newStatus: RuleExecutionStatus['partial failure'],
+ newStatus: RuleExecutionStatusEnum['partial failure'],
message:
'This rule is attempting to query data from Elasticsearch indices listed in the "Index patterns" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is disabled.',
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts
index 27d7df31b9d0a..55728183b8bfd 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts
@@ -32,7 +32,7 @@ import { parseDuration } from '@kbn/alerting-plugin/server';
import type { ExceptionListClient, ListClient, ListPluginSetup } from '@kbn/lists-plugin/server';
import type { TimestampOverride } from '../../../../../common/api/detection_engine/model/rule_schema';
import type { Privilege } from '../../../../../common/api/detection_engine';
-import { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring';
import type {
BulkResponseErrorAggregation,
SignalHit,
@@ -94,7 +94,7 @@ export const hasReadIndexPrivileges = async (args: {
const indexesString = JSON.stringify(indexesWithNoReadPrivileges);
warningStatusMessage = `This rule may not have the required read privileges to the following index patterns: ${indexesString}`;
await ruleExecutionLogger.logStatusChange({
- newStatus: RuleExecutionStatus['partial failure'],
+ newStatus: RuleExecutionStatusEnum['partial failure'],
message: warningStatusMessage,
});
return { wroteWarningMessage: true, warningStatusMessage };
@@ -129,7 +129,7 @@ export const hasTimestampFields = async (args: {
}`;
await ruleExecutionLogger.logStatusChange({
- newStatus: RuleExecutionStatus['partial failure'],
+ newStatus: RuleExecutionStatusEnum['partial failure'],
message: errorString.trimEnd(),
});
@@ -157,7 +157,7 @@ export const hasTimestampFields = async (args: {
)}`;
await ruleExecutionLogger.logStatusChange({
- newStatus: RuleExecutionStatus['partial failure'],
+ newStatus: RuleExecutionStatusEnum['partial failure'],
message: errorString,
});
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_create_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_create_handler.ts
index f87e845487a04..0e7a2db2d4e47 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_create_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_create_handler.ts
@@ -11,10 +11,11 @@ import type {
} from '@kbn/lists-plugin/server';
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import {
+ BlocklistValidator,
+ EndpointExceptionsValidator,
EventFilterValidator,
- TrustedAppValidator,
HostIsolationExceptionsValidator,
- BlocklistValidator,
+ TrustedAppValidator,
} from '../validators';
type ValidatorCallback = ExceptionsListPreCreateItemServerExtension['callback'];
@@ -65,6 +66,17 @@ export const getExceptionsPreCreateItemHandler = (
return validatedItem;
}
+ // validate endpoint exceptions
+ if (EndpointExceptionsValidator.isEndpointException(data)) {
+ const endpointExceptionValidator = new EndpointExceptionsValidator(
+ endpointAppContext,
+ request
+ );
+ const validatedItem = await endpointExceptionValidator.validatePreCreateItem(data);
+ endpointExceptionValidator.notifyFeatureUsage(data, 'ENDPOINT_EXCEPTIONS');
+ return validatedItem;
+ }
+
return data;
};
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_delete_item_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_delete_item_handler.ts
index 66a9b4709bd2b..719cd7b655b22 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_delete_item_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_delete_item_handler.ts
@@ -9,10 +9,11 @@ import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-t
import type { ExceptionsListPreDeleteItemServerExtension } from '@kbn/lists-plugin/server';
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import {
- TrustedAppValidator,
- HostIsolationExceptionsValidator,
- EventFilterValidator,
BlocklistValidator,
+ EndpointExceptionsValidator,
+ EventFilterValidator,
+ HostIsolationExceptionsValidator,
+ TrustedAppValidator,
} from '../validators';
type ValidatorCallback = ExceptionsListPreDeleteItemServerExtension['callback'];
@@ -64,6 +65,15 @@ export const getExceptionsPreDeleteItemHandler = (
return data;
}
+ // Validate Endpoint Exceptions
+ if (EndpointExceptionsValidator.isEndpointException({ listId })) {
+ await new EndpointExceptionsValidator(
+ endpointAppContextService,
+ request
+ ).validatePreDeleteItem();
+ return data;
+ }
+
return data;
};
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_export_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_export_handler.ts
index 7dac876b34f22..47ed89ce19982 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_export_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_export_handler.ts
@@ -8,10 +8,11 @@
import type { ExceptionsListPreExportServerExtension } from '@kbn/lists-plugin/server';
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import {
- TrustedAppValidator,
- HostIsolationExceptionsValidator,
- EventFilterValidator,
BlocklistValidator,
+ EndpointExceptionsValidator,
+ EventFilterValidator,
+ HostIsolationExceptionsValidator,
+ TrustedAppValidator,
} from '../validators';
type ValidatorCallback = ExceptionsListPreExportServerExtension['callback'];
@@ -61,6 +62,12 @@ export const getExceptionsPreExportHandler = (
return data;
}
+ // Validate Endpoint Exceptions
+ if (EndpointExceptionsValidator.isEndpointException({ listId })) {
+ await new EndpointExceptionsValidator(endpointAppContextService, request).validatePreExport();
+ return data;
+ }
+
return data;
};
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_get_one_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_get_one_handler.ts
index 9ed81b1f0d585..15e9af5e61ac9 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_get_one_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_get_one_handler.ts
@@ -9,10 +9,11 @@ import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-t
import type { ExceptionsListPreGetOneItemServerExtension } from '@kbn/lists-plugin/server';
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import {
- TrustedAppValidator,
- HostIsolationExceptionsValidator,
- EventFilterValidator,
BlocklistValidator,
+ EndpointExceptionsValidator,
+ EventFilterValidator,
+ HostIsolationExceptionsValidator,
+ TrustedAppValidator,
} from '../validators';
type ValidatorCallback = ExceptionsListPreGetOneItemServerExtension['callback'];
@@ -64,6 +65,15 @@ export const getExceptionsPreGetOneHandler = (
return data;
}
+ // Validate Endpoint Exceptions
+ if (EndpointExceptionsValidator.isEndpointException({ listId })) {
+ await new EndpointExceptionsValidator(
+ endpointAppContextService,
+ request
+ ).validatePreGetOneItem();
+ return data;
+ }
+
return data;
};
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_multi_list_find_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_multi_list_find_handler.ts
index 973bb6ce5072a..e3e66daebc57d 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_multi_list_find_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_multi_list_find_handler.ts
@@ -8,10 +8,11 @@
import type { ExceptionsListPreMultiListFindServerExtension } from '@kbn/lists-plugin/server';
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import {
- TrustedAppValidator,
- HostIsolationExceptionsValidator,
- EventFilterValidator,
BlocklistValidator,
+ EndpointExceptionsValidator,
+ EventFilterValidator,
+ HostIsolationExceptionsValidator,
+ TrustedAppValidator,
} from '../validators';
type ValidatorCallback = ExceptionsListPreMultiListFindServerExtension['callback'];
@@ -54,6 +55,15 @@ export const getExceptionsPreMultiListFindHandler = (
return data;
}
+ // Validate Endpoint Exceptions
+ if (data.listId.some((id) => EndpointExceptionsValidator.isEndpointException({ listId: id }))) {
+ await new EndpointExceptionsValidator(
+ endpointAppContextService,
+ request
+ ).validatePreMultiListFind();
+ return data;
+ }
+
return data;
};
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_single_list_find_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_single_list_find_handler.ts
index 946a0bf6d7c43..e647ea2a710f5 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_single_list_find_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_single_list_find_handler.ts
@@ -8,10 +8,11 @@
import type { ExceptionsListPreSingleListFindServerExtension } from '@kbn/lists-plugin/server';
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import {
- TrustedAppValidator,
- HostIsolationExceptionsValidator,
- EventFilterValidator,
BlocklistValidator,
+ EndpointExceptionsValidator,
+ EventFilterValidator,
+ HostIsolationExceptionsValidator,
+ TrustedAppValidator,
} from '../validators';
type ValidatorCallback = ExceptionsListPreSingleListFindServerExtension['callback'];
@@ -55,6 +56,15 @@ export const getExceptionsPreSingleListFindHandler = (
return data;
}
+ // Validate Endpoint Exceptions
+ if (EndpointExceptionsValidator.isEndpointException({ listId })) {
+ await new EndpointExceptionsValidator(
+ endpointAppContextService,
+ request
+ ).validatePreSingleListFind();
+ return data;
+ }
+
return data;
};
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_summary_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_summary_handler.ts
index 6b9af37f877ab..d50504572b369 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_summary_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_summary_handler.ts
@@ -8,10 +8,11 @@
import type { ExceptionsListPreSummaryServerExtension } from '@kbn/lists-plugin/server';
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import {
- TrustedAppValidator,
- HostIsolationExceptionsValidator,
- EventFilterValidator,
BlocklistValidator,
+ EndpointExceptionsValidator,
+ EventFilterValidator,
+ HostIsolationExceptionsValidator,
+ TrustedAppValidator,
} from '../validators';
type ValidatorCallback = ExceptionsListPreSummaryServerExtension['callback'];
@@ -61,6 +62,15 @@ export const getExceptionsPreSummaryHandler = (
return data;
}
+ // Validate Endpoint Exceptions
+ if (EndpointExceptionsValidator.isEndpointException({ listId })) {
+ await new EndpointExceptionsValidator(
+ endpointAppContextService,
+ request
+ ).validatePreGetListSummary();
+ return data;
+ }
+
return data;
};
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts
index 681d16a1e44b8..810b569ecf8a6 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts
@@ -12,10 +12,11 @@ import type {
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import type { ExceptionItemLikeOptions } from '../types';
import {
+ BlocklistValidator,
+ EndpointExceptionsValidator,
EventFilterValidator,
- TrustedAppValidator,
HostIsolationExceptionsValidator,
- BlocklistValidator,
+ TrustedAppValidator,
} from '../validators';
type ValidatorCallback = ExceptionsListPreUpdateItemServerExtension['callback'];
@@ -98,6 +99,20 @@ export const getExceptionsPreUpdateItemHandler = (
return validatedItem;
}
+ // Validate Endpoint Exceptions
+ if (EndpointExceptionsValidator.isEndpointException({ listId })) {
+ const endpointExceptionValidator = new EndpointExceptionsValidator(
+ endpointAppContextService,
+ request
+ );
+ const validatedItem = await endpointExceptionValidator.validatePreUpdateItem(data);
+ endpointExceptionValidator.notifyFeatureUsage(
+ data as ExceptionItemLikeOptions,
+ 'ENDPOINT_EXCEPTIONS'
+ );
+ return validatedItem;
+ }
+
return data;
};
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts
index ecc2ac7893336..4630ad9edec07 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/base_validator.ts
@@ -10,6 +10,7 @@ import { schema } from '@kbn/config-schema';
import { isEqual } from 'lodash/fp';
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { OperatingSystem } from '@kbn/securitysolution-utils';
+
import type { EndpointAuthz } from '../../../../common/endpoint/types/authz';
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
import type { ExceptionItemLikeOptions } from '../types';
@@ -19,6 +20,7 @@ import {
isArtifactByPolicy,
} from '../../../../common/endpoint/service/artifacts';
import { EndpointArtifactExceptionValidationError } from './errors';
+import { EndpointExceptionsValidationError } from './endpoint_exception_errors';
import type { FeatureKeys } from '../../../endpoint/services/feature_usage/service';
export const BasicEndpointExceptionDataSchema = schema.object(
@@ -74,6 +76,14 @@ export class BaseValidator {
}
}
+ protected async validateHasEndpointExceptionsPrivileges(
+ privilege: keyof EndpointAuthz
+ ): Promise {
+ if (!(await this.endpointAuthzPromise)[privilege]) {
+ throw new EndpointExceptionsValidationError('Endpoint exceptions authorization failure', 403);
+ }
+ }
+
protected async validateHasPrivilege(privilege: keyof EndpointAuthz): Promise {
if (!(await this.endpointAuthzPromise)[privilege]) {
throw new EndpointArtifactExceptionValidationError('Endpoint authorization failure', 403);
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exception_errors.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exception_errors.ts
new file mode 100644
index 0000000000000..d0f2ba0537b55
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exception_errors.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 { ListsErrorWithStatusCode } from '@kbn/lists-plugin/server';
+
+export class EndpointExceptionsValidationError extends ListsErrorWithStatusCode {
+ constructor(message: string, statusCode: number = 400) {
+ super(`EndpointExceptionsError: ${message}`, statusCode);
+ }
+}
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exceptions_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exceptions_validator.ts
new file mode 100644
index 0000000000000..23d1d28ba0a59
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/endpoint_exceptions_validator.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 {
+ CreateExceptionListItemOptions,
+ UpdateExceptionListItemOptions,
+} from '@kbn/lists-plugin/server';
+import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants';
+import { BaseValidator } from './base_validator';
+
+export class EndpointExceptionsValidator extends BaseValidator {
+ static isEndpointException(item: { listId: string }): boolean {
+ return item.listId === ENDPOINT_LIST_ID;
+ }
+
+ protected async validateHasReadPrivilege(): Promise {
+ return this.validateHasEndpointExceptionsPrivileges('canReadEndpointExceptions');
+ }
+
+ protected async validateHasWritePrivilege(): Promise {
+ return this.validateHasEndpointExceptionsPrivileges('canWriteEndpointExceptions');
+ }
+
+ async validatePreCreateItem(item: CreateExceptionListItemOptions) {
+ await this.validateHasWritePrivilege();
+ return item;
+ }
+
+ async validatePreUpdateItem(item: UpdateExceptionListItemOptions) {
+ await this.validateHasWritePrivilege();
+ return item;
+ }
+
+ async validatePreDeleteItem(): Promise {
+ await this.validateHasWritePrivilege();
+ }
+
+ async validatePreGetOneItem(): Promise {
+ await this.validateHasReadPrivilege();
+ }
+
+ async validatePreMultiListFind(): Promise {
+ await this.validateHasReadPrivilege();
+ }
+
+ async validatePreExport(): Promise {
+ await this.validateHasReadPrivilege();
+ }
+
+ async validatePreSingleListFind(): Promise {
+ await this.validateHasReadPrivilege();
+ }
+
+ async validatePreGetListSummary(): Promise {
+ await this.validateHasReadPrivilege();
+ }
+}
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/index.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/index.ts
index ccd6ebd8e08d6..8dd357b15ebd9 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/index.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/index.ts
@@ -9,3 +9,4 @@ export { TrustedAppValidator } from './trusted_app_validator';
export { EventFilterValidator } from './event_filter_validator';
export { HostIsolationExceptionsValidator } from './host_isolation_exceptions_validator';
export { BlocklistValidator } from './blocklist_validator';
+export { EndpointExceptionsValidator } from './endpoint_exceptions_validator';
diff --git a/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts b/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts
index 57692b515a230..a6e41257123ab 100644
--- a/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts
+++ b/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts
@@ -14,6 +14,8 @@ import type {
RouteValidationResultFactory,
RouteValidationError,
} from '@kbn/core/server';
+import type { TypeOf, ZodType } from 'zod';
+import { stringifyZodError } from '@kbn/securitysolution-es-utils';
import type { GenericIntersectionC } from '../runtime_types';
import { excess } from '../runtime_types';
@@ -65,3 +67,14 @@ export const buildRouteValidationWithExcess =
(validatedInput: A) => validationResult.ok(validatedInput)
)
);
+
+export const buildRouteValidationWithZod =
+ >(schema: T): RouteValidationFunction =>
+ (inputValue: unknown, validationResult: RouteValidationResultFactory) => {
+ const decoded = schema.safeParse(inputValue);
+ if (decoded.success) {
+ return validationResult.ok(decoded.data);
+ } else {
+ return validationResult.badRequest(stringifyZodError(decoded.error));
+ }
+ };
diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json
index 19ced3697e2c9..de11312c2f60e 100644
--- a/x-pack/plugins/security_solution/tsconfig.json
+++ b/x-pack/plugins/security_solution/tsconfig.json
@@ -176,6 +176,7 @@
"@kbn/subscription-tracking",
"@kbn/core-application-common",
"@kbn/openapi-generator",
- "@kbn/es"
+ "@kbn/es",
+ "@kbn/react-kibana-mount"
]
}
diff --git a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts
index 1ef3da121b910..62acff6857a8e 100644
--- a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts
+++ b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts
@@ -33,6 +33,7 @@ export const PLI_APP_FEATURES: PliAppFeatures = {
complete: [
AppFeatureKey.endpointResponseActions,
AppFeatureKey.osqueryAutomatedResponseActions,
+ AppFeatureKey.endpointExceptions,
],
},
cloud: {
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx
index e24434ea0b9e8..71f787e19c3bd 100644
--- a/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx
@@ -27,6 +27,10 @@ export const OsqueryResponseActionsUpsellingSectionLazy = withSuspenseUpsell(
lazy(() => import('./pages/osquery_automated_response_actions'))
);
+export const EndpointExceptionsDetailsUpsellingLazy = withSuspenseUpsell(
+ lazy(() => import('./pages/endpoint_management/endpoint_exceptions_details'))
+);
+
export const EntityAnalyticsUpsellingLazy = withSuspenseUpsell(
lazy(() => import('@kbn/security-solution-upselling/pages/entity_analytics'))
);
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/pages/endpoint_management/endpoint_exceptions_details.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/pages/endpoint_management/endpoint_exceptions_details.tsx
new file mode 100644
index 0000000000000..fd514350eaf0a
--- /dev/null
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/pages/endpoint_management/endpoint_exceptions_details.tsx
@@ -0,0 +1,47 @@
+/*
+ * 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 { EuiEmptyPrompt, EuiIcon } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import React, { memo } from 'react';
+import type { AppFeatureKeyType } from '@kbn/security-solution-features/keys';
+import { getProductTypeByPLI } from '../../hooks/use_product_type_by_pli';
+
+const EndpointExceptionsDetailsUpselling: React.FC<{ requiredPLI: AppFeatureKeyType }> = memo(
+ ({ requiredPLI }) => {
+ const productTypeRequired = getProductTypeByPLI(requiredPLI);
+
+ return (
+ }
+ color="subdued"
+ title={
+
+
+
+ }
+ body={
+
+
+
+ }
+ />
+ );
+ }
+);
+
+EndpointExceptionsDetailsUpselling.displayName = 'EndpointExceptionsDetailsUpselling';
+
+// eslint-disable-next-line import/no-default-export
+export { EndpointExceptionsDetailsUpselling as default };
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/pages/osquery_automated_response_actions.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/pages/osquery_automated_response_actions.tsx
index 3097d41819058..b1abd3400797c 100644
--- a/x-pack/plugins/security_solution_serverless/public/upselling/pages/osquery_automated_response_actions.tsx
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/pages/osquery_automated_response_actions.tsx
@@ -8,6 +8,7 @@
import { EuiEmptyPrompt, EuiIcon } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
+
import type { AppFeatureKeyType } from '@kbn/security-solution-features';
import { getProductTypeByPLI } from '../hooks/use_product_type_by_pli';
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx
index f1b8da6b1557d..4c9f9f65fc0a7 100644
--- a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx
@@ -17,10 +17,14 @@ import React from 'react';
import { UPGRADE_INVESTIGATION_GUIDE } from '@kbn/security-solution-upselling/messages';
import { AppFeatureKey } from '@kbn/security-solution-features/keys';
import type { AppFeatureKeyType } from '@kbn/security-solution-features';
-import { EndpointPolicyProtectionsLazy } from './sections/endpoint_management';
+import {
+ EndpointPolicyProtectionsLazy,
+ RuleDetailsEndpointExceptionsLazy,
+} from './sections/endpoint_management';
import type { SecurityProductTypes } from '../../common/config';
import { getProductAppFeatures } from '../../common/pli/pli_features';
import {
+ EndpointExceptionsDetailsUpsellingLazy,
EntityAnalyticsUpsellingLazy,
OsqueryResponseActionsUpsellingSectionLazy,
ThreatIntelligencePaywallLazy,
@@ -86,7 +90,7 @@ export const registerUpsellings = (
upselling.setMessages(upsellingMessagesToRegister);
};
-// Upsellings for entire pages, linked to a SecurityPageName
+// Upselling for entire pages, linked to a SecurityPageName
export const upsellingPages: UpsellingPages = [
// It is highly advisable to make use of lazy loaded components to minimize bundle size.
{
@@ -105,9 +109,16 @@ export const upsellingPages: UpsellingPages = [
),
},
+ {
+ pageName: SecurityPageName.exceptions,
+ pli: AppFeatureKey.endpointExceptions,
+ component: () => (
+
+ ),
+ },
];
-// Upsellings for sections, linked by arbitrary ids
+// Upselling for sections, linked by arbitrary ids
export const upsellingSections: UpsellingSections = [
// It is highly advisable to make use of lazy loaded components to minimize bundle size.
{
@@ -124,9 +135,14 @@ export const upsellingSections: UpsellingSections = [
pli: AppFeatureKey.endpointPolicyProtections,
component: EndpointPolicyProtectionsLazy,
},
+ {
+ id: 'ruleDetailsEndpointExceptions',
+ pli: AppFeatureKey.endpointExceptions,
+ component: RuleDetailsEndpointExceptionsLazy,
+ },
];
-// Upsellings for sections, linked by arbitrary ids
+// Upselling for sections, linked by arbitrary ids
export const upsellingMessages: UpsellingMessages = [
{
id: 'investigation_guide',
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts
index a76b1cc0bacc8..e6db03492885e 100644
--- a/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/index.ts
@@ -12,3 +12,9 @@ export const EndpointPolicyProtectionsLazy = lazy(() =>
default: EndpointPolicyProtections,
}))
);
+
+export const RuleDetailsEndpointExceptionsLazy = lazy(() =>
+ import('./rule_details_endpoint_exceptions').then(({ RuleDetailsEndpointExceptions }) => ({
+ default: RuleDetailsEndpointExceptions,
+ }))
+);
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/rule_details_endpoint_exceptions.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/rule_details_endpoint_exceptions.tsx
new file mode 100644
index 0000000000000..39bf086d7558b
--- /dev/null
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management/rule_details_endpoint_exceptions.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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, { memo } from 'react';
+import { EuiCard, EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import styled from '@emotion/styled';
+
+const BADGE_TEXT = i18n.translate(
+ 'xpack.securitySolutionServerless.rules.endpointSecurity.endpointExceptions.badgeText',
+ {
+ defaultMessage: 'Endpoint Essentials',
+ }
+);
+const CARD_TITLE = i18n.translate(
+ 'xpack.securitySolutionServerless.rules.endpointSecurity.endpointExceptions.cardTitle',
+ {
+ defaultMessage: 'Do more with Security!',
+ }
+);
+const CARD_MESSAGE = i18n.translate(
+ 'xpack.securitySolutionServerless.rules.endpointSecurity.endpointExceptions.cardMessage',
+ {
+ defaultMessage:
+ 'Upgrade your license to {productTypeRequired} to use Endpoint Security Exception List.',
+ values: { productTypeRequired: BADGE_TEXT },
+ }
+);
+
+const CardDescription = styled.p`
+ padding: 0 33.3%;
+`;
+
+/**
+ * Component displayed trying to access endpoint exceptions tab on Endpoint security rule details.
+ */
+export const RuleDetailsEndpointExceptions = memo(() => {
+ return (
+ }
+ betaBadgeProps={{
+ 'data-test-subj': 'rules-endpointSecurity-endpointExceptionsLockedCard-badge',
+ label: BADGE_TEXT,
+ }}
+ title={
+
+ {CARD_TITLE}
+
+ }
+ >
+ {CARD_MESSAGE}
+
+ );
+});
+RuleDetailsEndpointExceptions.displayName = 'RuleDetailsEndpointExceptions';
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/action_variables.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/action_variables.ts
index 7ec8b3e3d5f6d..926d9c68be069 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/action_variables.ts
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/action_variables.ts
@@ -16,6 +16,17 @@ export const casesVars: ActionVariable[] = [
useWithTripleBracesInTemplates: true,
},
{ name: 'case.tags', description: i18n.CASE_TAGS_DESC, useWithTripleBracesInTemplates: true },
+ { name: 'case.id', description: i18n.CASE_ID_DESC, useWithTripleBracesInTemplates: true },
+ {
+ name: 'case.status',
+ description: i18n.CASE_SEVERITY_DESC,
+ useWithTripleBracesInTemplates: true,
+ },
+ {
+ name: 'case.severity',
+ description: i18n.CASE_STATUS_DESC,
+ useWithTripleBracesInTemplates: true,
+ },
];
export const commentVars: ActionVariable[] = [
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/severity_filter.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/severity_filter.tsx
new file mode 100644
index 0000000000000..619f4c58d12df
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/severity_filter.tsx
@@ -0,0 +1,81 @@
+/*
+ * 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 { euiLightVars } from '@kbn/ui-theme';
+import React from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHealth,
+ EuiSuperSelect,
+ EuiSuperSelectOption,
+} from '@elastic/eui';
+import * as i18n from './translations';
+
+export enum CaseSeverity {
+ LOW = 'low',
+ MEDIUM = 'medium',
+ HIGH = 'high',
+ CRITICAL = 'critical',
+}
+
+export const severities = {
+ [CaseSeverity.LOW]: {
+ color: euiLightVars.euiColorVis0,
+ label: i18n.SEVERITY_LOW_LABEL,
+ },
+ [CaseSeverity.MEDIUM]: {
+ color: euiLightVars.euiColorVis5,
+ label: i18n.SEVERITY_MEDIUM_LABEL,
+ },
+ [CaseSeverity.HIGH]: {
+ color: euiLightVars.euiColorVis7,
+ label: i18n.SEVERITY_HIGH_LABEL,
+ },
+ [CaseSeverity.CRITICAL]: {
+ color: euiLightVars.euiColorVis9,
+ label: i18n.SEVERITY_CRITICAL_LABEL,
+ },
+};
+
+interface Props {
+ selectedSeverity: CaseSeverity;
+ onSeverityChange: (status: CaseSeverity) => void;
+}
+
+export const SeverityFilter: React.FC = ({ selectedSeverity, onSeverityChange }) => {
+ const caseSeverities = Object.keys(severities) as CaseSeverity[];
+ const options: Array> = caseSeverities.map((severity) => {
+ const severityData = severities[severity];
+ return {
+ value: severity,
+ inputDisplay: (
+
+
+ {severityData.label}
+
+
+ ),
+ };
+ });
+
+ return (
+
+ );
+};
+SeverityFilter.displayName = 'SeverityFilter';
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/status_filter.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/status_filter.tsx
new file mode 100644
index 0000000000000..01569027809f7
--- /dev/null
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/status_filter.tsx
@@ -0,0 +1,73 @@
+/*
+ * 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 { CaseStatuses } from '@kbn/cases-components';
+import {
+ EuiBadge,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSuperSelect,
+ EuiSuperSelectOption,
+} from '@elastic/eui';
+
+import * as i18n from './translations';
+
+interface Props {
+ selectedStatus: CaseStatuses;
+ onStatusChanged: (status: CaseStatuses) => void;
+}
+
+const caseStatuses = [CaseStatuses.open, CaseStatuses['in-progress'], CaseStatuses.closed];
+const statuses = {
+ [CaseStatuses.open]: {
+ color: 'primary',
+ label: i18n.STATUS_OPEN,
+ },
+ [CaseStatuses['in-progress']]: {
+ color: 'warning',
+ label: i18n.STATUS_IN_PROGRESS,
+ },
+ [CaseStatuses.closed]: {
+ color: 'default',
+ label: i18n.STATUS_CLOSED,
+ },
+} as const;
+
+export const StatusFilter: React.FC = ({ selectedStatus, onStatusChanged }) => {
+ const options: Array> = caseStatuses.map((status) => ({
+ value: status,
+ inputDisplay: (
+
+
+
+
+ {statuses[status]?.label}
+
+
+
+
+ ),
+ 'data-test-subj': `case-status-filter-${status}`,
+ }));
+
+ return (
+
+ );
+};
+
+StatusFilter.displayName = 'StatusFilter';
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts
index bbe7b09bd4509..cf5b0a37a06dd 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts
@@ -458,6 +458,27 @@ export const CASE_TAGS_DESC = i18n.translate(
}
);
+export const CASE_ID_DESC = i18n.translate(
+ 'xpack.stackConnectors.components.casesWebhook.caseIdDesc',
+ {
+ defaultMessage: 'Kibana case id',
+ }
+);
+
+export const CASE_SEVERITY_DESC = i18n.translate(
+ 'xpack.stackConnectors.components.casesWebhook.caseSeverityDesc',
+ {
+ defaultMessage: 'Kibana case severity',
+ }
+);
+
+export const CASE_STATUS_DESC = i18n.translate(
+ 'xpack.stackConnectors.components.casesWebhook.caseStatusDesc',
+ {
+ defaultMessage: 'Kibana case status',
+ }
+);
+
export const CASE_COMMENT_DESC = i18n.translate(
'xpack.stackConnectors.components.casesWebhook.caseCommentDesc',
{
@@ -482,3 +503,47 @@ export const EXTERNAL_TITLE_DESC = i18n.translate(
export const DOC_LINK = i18n.translate('xpack.stackConnectors.components.casesWebhook.docLink', {
defaultMessage: 'Configuring Webhook - Case Management connector.',
});
+
+export const SEVERITY_CRITICAL_LABEL = i18n.translate(
+ 'xpack.stackConnectors.components.casesWebhook.criticalLabel',
+ {
+ defaultMessage: 'Critical',
+ }
+);
+export const SEVERITY_HIGH_LABEL = i18n.translate(
+ 'xpack.stackConnectors.components.casesWebhook.highLabel',
+ {
+ defaultMessage: 'High',
+ }
+);
+export const SEVERITY_MEDIUM_LABEL = i18n.translate(
+ 'xpack.stackConnectors.components.casesWebhook.mediumLabel',
+ {
+ defaultMessage: 'Medium',
+ }
+);
+export const SEVERITY_LOW_LABEL = i18n.translate(
+ 'xpack.stackConnectors.components.casesWebhook.lowLabel',
+ {
+ defaultMessage: 'Low',
+ }
+);
+
+export const STATUS_OPEN = i18n.translate(
+ 'xpack.stackConnectors.components.casesWebhook.status.open',
+ {
+ defaultMessage: 'Open',
+ }
+);
+export const STATUS_CLOSED = i18n.translate(
+ 'xpack.stackConnectors.components.casesWebhook.status.closed',
+ {
+ defaultMessage: 'Closed',
+ }
+);
+export const STATUS_IN_PROGRESS = i18n.translate(
+ 'xpack.stackConnectors.components.casesWebhook.status.inProgress',
+ {
+ defaultMessage: 'In progress',
+ }
+);
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_params.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_params.test.tsx
index b5e2cd3d10871..5e7d85a85d247 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_params.test.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_params.test.tsx
@@ -31,6 +31,9 @@ const actionParams = {
description: 'some description',
tags: ['kibana'],
externalId: null,
+ id: '10006',
+ severity: 'High',
+ status: 'Open',
},
comments: [],
},
@@ -65,6 +68,10 @@ describe('WebhookParamsFields renders', () => {
expect(wrapper.find('[data-test-subj="descriptionTextArea"]').length > 0).toBeTruthy();
expect(wrapper.find('[data-test-subj="tagsComboBox"]').length > 0).toBeTruthy();
expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="tagsComboBox"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="case-severity-selection"]').length > 0).toBeTruthy();
+ expect(wrapper.find('[data-test-subj="case-status-filter"]').length > 0).toBeTruthy();
expect(wrapper.find('[data-test-subj="commentsTextArea"]').first().prop('disabled')).toEqual(
false
);
diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_params.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_params.tsx
index dab3f8cc95832..72bd3ea8ea843 100644
--- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_params.tsx
+++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_params.tsx
@@ -13,6 +13,9 @@ import {
TextAreaWithMessageVariables,
TextFieldWithMessageVariables,
} from '@kbn/triggers-actions-ui-plugin/public';
+import { CaseStatuses } from '@kbn/cases-components';
+import { CaseSeverity, SeverityFilter } from './severity_filter';
+import { StatusFilter } from './status_filter';
import { CasesWebhookActionConnector, CasesWebhookActionParams } from './types';
const CREATE_COMMENT_WARNING_TITLE = i18n.translate(
@@ -169,6 +172,52 @@ const WebhookParamsFields: React.FunctionComponent
+
+ editSubActionProperty('status', status)}
+ />
+
+
+ editSubActionProperty('severity', severity)}
+ />
+
+ 0 &&
+ incident.id !== undefined
+ }
+ label={i18n.translate('xpack.stackConnectors.components.casesWebhook.idFieldLabel', {
+ defaultMessage: 'Case ID',
+ })}
+ >
+
+
<>
{
incident: {
tags: ['kibana', 'elastic'],
description: 'Incident description',
+ id: '10006',
+ severity: 'High',
+ status: 'Open',
title: 'Incident title',
},
});
@@ -148,6 +151,9 @@ describe('api', () => {
incident: {
tags: ['kibana', 'elastic'],
description: 'Incident description',
+ id: '10006',
+ severity: 'High',
+ status: 'Open',
title: 'Incident title',
},
});
@@ -162,6 +168,9 @@ describe('api', () => {
incidentId: 'incident-3',
incident: {
description: 'Incident description',
+ id: '10006',
+ severity: 'High',
+ status: 'Open',
title: 'Incident title',
tags: ['kibana', 'elastic'],
},
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/mock.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/mock.ts
index 74627658cde81..e6d39dd0d6fe0 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/mock.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/mock.ts
@@ -67,6 +67,9 @@ const executorParams: ExecutorSubActionPushParams = {
title: 'Incident title',
description: 'Incident description',
tags: ['kibana', 'elastic'],
+ id: '10006',
+ severity: 'High',
+ status: 'Open',
},
comments: [
{
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/schema.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/schema.ts
index 1fb3edca6e4b7..9863b1457628a 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/schema.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/schema.ts
@@ -71,6 +71,9 @@ export const ExecutorSubActionPushParamsSchema = schema.object({
incident: schema.object({
title: schema.string(),
description: schema.nullable(schema.string()),
+ id: schema.nullable(schema.string()),
+ severity: schema.nullable(schema.string()),
+ status: schema.nullable(schema.string()),
externalId: schema.nullable(schema.string()),
tags: schema.nullable(schema.arrayOf(schema.string())),
}),
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.test.ts
index f0d2105f37fdc..4598e8471a838 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.test.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.test.ts
@@ -197,9 +197,9 @@ describe('Cases webhook service', () => {
title: 'title',
description: 'desc',
tags: ['hello', 'world'],
- issueType: '10006',
- priority: 'High',
- parent: 'RJ-107',
+ id: '10006',
+ severity: 'High',
+ status: 'Open',
},
};
@@ -302,6 +302,9 @@ describe('Cases webhook service', () => {
title: 'title',
description: 'desc',
tags: ['hello', 'world'],
+ id: '10006',
+ severity: 'High',
+ status: 'Open',
},
};
@@ -579,9 +582,9 @@ describe('Cases webhook service', () => {
title: 'title',
description: 'desc',
tags: ['hello', 'world'],
- issueType: '10006',
- priority: 'High',
- parent: 'RJ-107',
+ id: '10006',
+ severity: 'High',
+ status: 'Open',
},
};
@@ -596,9 +599,9 @@ describe('Cases webhook service', () => {
title: 'title',
description: 'desc',
tags: ['hello', 'world'],
- issueType: '10006',
- priority: 'High',
- parent: 'RJ-107',
+ id: '10006',
+ severity: 'High',
+ status: 'Open',
},
};
@@ -653,9 +656,9 @@ describe('Cases webhook service', () => {
title: 'title',
description: 'desc',
tags: ['hello', 'world'],
- issueType: '10006',
- priority: 'High',
- parent: 'RJ-107',
+ id: '10006',
+ severity: 'High',
+ status: 'Open',
},
};
@@ -670,9 +673,9 @@ describe('Cases webhook service', () => {
title: 'title',
description: 'desc',
tags: ['hello', 'world'],
- issueType: '10006',
- priority: 'High',
- parent: 'RJ-107',
+ id: '10006',
+ severity: 'High',
+ status: 'Open',
},
};
@@ -733,9 +736,9 @@ describe('Cases webhook service', () => {
title: 'title',
description: 'desc',
tags: ['hello', 'world'],
- issueType: '10006',
- priority: 'High',
- parent: 'RJ-107',
+ id: '10006',
+ severity: 'High',
+ status: 'Open',
},
};
const res = await service.createIncident(incident);
@@ -751,9 +754,9 @@ describe('Cases webhook service', () => {
title: 'title',
description: 'desc',
tags: ['hello', 'world'],
- issueType: '10006',
- priority: 'High',
- parent: 'RJ-107',
+ id: '10006',
+ severity: 'High',
+ status: 'Open',
},
};
diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts
index 790bdffc84982..dd0b28d321a60 100644
--- a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts
+++ b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/service.ts
@@ -118,7 +118,7 @@ export const createExternalService = (
incident,
}: CreateIncidentParams): Promise => {
try {
- const { tags, title, description } = incident;
+ const { description, id, severity, status: incidentStatus, tags, title } = incident;
const normalizedUrl = validateAndNormalizeUrl(
`${createIncidentUrl}`,
configurationUtilities,
@@ -127,9 +127,12 @@ export const createExternalService = (
const json = renderMustacheStringNoEscape(
createIncidentJson,
stringifyObjValues({
- title,
description: description ?? '',
+ id: id ?? '',
+ severity: severity ?? '',
+ status: incidentStatus ?? '',
tags: tags ?? [],
+ title,
})
);
@@ -197,13 +200,16 @@ export const createExternalService = (
'Update case URL'
);
- const { tags, title, description } = incident;
+ const { description, id, severity, status: incidentStatus, tags, title } = incident;
const json = renderMustacheStringNoEscape(updateIncidentJson, {
...stringifyObjValues({
- title,
description: description ?? '',
+ id: id ?? '',
+ severity: severity ?? '',
+ status: incidentStatus ?? '',
tags: tags ?? [],
+ title,
}),
external: {
system: {
diff --git a/x-pack/plugins/stack_connectors/tsconfig.json b/x-pack/plugins/stack_connectors/tsconfig.json
index 0009b86c8348c..4e47b703bf433 100644
--- a/x-pack/plugins/stack_connectors/tsconfig.json
+++ b/x-pack/plugins/stack_connectors/tsconfig.json
@@ -37,6 +37,7 @@
"@kbn/alerting-plugin",
"@kbn/securitysolution-ecs",
"@kbn/ui-theme",
+ "@kbn/cases-components",
],
"exclude": [
"target/**/*",
diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
index d406fb1aa5ffc..db7cac9e55cd3 100644
--- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
+++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
@@ -4527,6 +4527,16 @@
}
}
},
+ "global_labels": {
+ "properties": {
+ "1d": {
+ "type": "long",
+ "_meta": {
+ "description": "Total number of global labels used for creating aggregation keys for internal metrics computed from indices which received data in the last 24 hours"
+ }
+ }
+ }
+ },
"max_transaction_groups_per_service": {
"properties": {
"1d": {
@@ -5927,6 +5937,20 @@
}
}
},
+ "global_labels": {
+ "properties": {
+ "took": {
+ "properties": {
+ "ms": {
+ "type": "long",
+ "_meta": {
+ "description": "Execution time in milliseconds for the \"global_labels\" task"
+ }
+ }
+ }
+ }
+ }
+ },
"services": {
"properties": {
"took": {
diff --git a/x-pack/plugins/transform/kibana.jsonc b/x-pack/plugins/transform/kibana.jsonc
index 4baea3243370a..ef3577846238a 100644
--- a/x-pack/plugins/transform/kibana.jsonc
+++ b/x-pack/plugins/transform/kibana.jsonc
@@ -29,6 +29,7 @@
"contentManagement",
],
"optionalPlugins": [
+ "dataViewEditor",
"security",
"usageCollection",
"spaces",
diff --git a/x-pack/plugins/transform/public/app/app_dependencies.tsx b/x-pack/plugins/transform/public/app/app_dependencies.tsx
index 93d74ca706d43..05c59bb66baa8 100644
--- a/x-pack/plugins/transform/public/app/app_dependencies.tsx
+++ b/x-pack/plugins/transform/public/app/app_dependencies.tsx
@@ -37,6 +37,7 @@ import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
+import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { GetMlSharedImportsReturnType } from '../shared_imports';
export interface AppDependencies {
@@ -45,6 +46,7 @@ export interface AppDependencies {
charts: ChartsPluginStart;
chrome: ChromeStart;
data: DataPublicPluginStart;
+ dataViewEditor?: DataViewEditorStart;
dataViews: DataViewsPublicPluginStart;
docLinks: DocLinksStart;
fieldFormats: FieldFormatsStart;
diff --git a/x-pack/plugins/transform/public/app/mount_management_section.ts b/x-pack/plugins/transform/public/app/mount_management_section.ts
index f00596e1326b4..e6aad082acd6e 100644
--- a/x-pack/plugins/transform/public/app/mount_management_section.ts
+++ b/x-pack/plugins/transform/public/app/mount_management_section.ts
@@ -46,6 +46,7 @@ export async function mountManagementSection(
const {
data,
dataViews,
+ dataViewEditor,
share,
spaces,
triggersActionsUi,
@@ -69,6 +70,7 @@ export async function mountManagementSection(
application,
chrome,
data,
+ dataViewEditor,
dataViews,
docLinks,
http,
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx
index 3f2bd7fb70c0c..75731baf92c06 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx
@@ -5,22 +5,49 @@
* 2.0.
*/
-import { EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui';
+import { EuiButton, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
-import React, { type FC } from 'react';
+import React, { type FC, Fragment, useCallback, useEffect, useRef } from 'react';
import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public';
import { useAppDependencies } from '../../../../app_dependencies';
interface SearchSelectionProps {
onSearchSelected: (searchId: string, searchType: string) => void;
+ onCloseModal: () => void;
}
const fixedPageSize: number = 8;
-export const SearchSelection: FC = ({ onSearchSelected }) => {
- const { contentManagement, uiSettings } = useAppDependencies();
+export const SearchSelection: FC = ({ onSearchSelected, onCloseModal }) => {
+ const { contentManagement, uiSettings, dataViewEditor } = useAppDependencies();
+
+ const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView());
+
+ const closeDataViewEditor = useRef<() => void | undefined>();
+
+ const createNewDataView = useCallback(() => {
+ onCloseModal();
+ closeDataViewEditor.current = dataViewEditor?.openEditor({
+ onSave: async (dataView) => {
+ if (dataView.id) {
+ onSearchSelected(dataView.id, 'index-pattern');
+ }
+ },
+
+ allowAdHocDataView: true,
+ });
+ }, [dataViewEditor, onCloseModal, onSearchSelected]);
+
+ useEffect(function cleanUpFlyout() {
+ return () => {
+ // Close the editor when unmounting
+ if (closeDataViewEditor.current) {
+ closeDataViewEditor.current();
+ }
+ };
+ }, []);
return (
<>
@@ -72,7 +99,24 @@ export const SearchSelection: FC = ({ onSearchSelected })
]}
fixedPageSize={fixedPageSize}
services={{ contentClient: contentManagement.client, uiSettings }}
- />
+ >
+ {canEditDataView ? (
+
+
+
+ ) : (
+
+ )}
+
>
);
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx
index 7e1f2ccd3bc09..b88d7fc88a7ce 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx
@@ -330,7 +330,7 @@ export const TransformManagement: FC = () => {
className="transformCreateTransformSearchDialog"
data-test-subj="transformSelectSourceModal"
>
-
+
)}
>
diff --git a/x-pack/plugins/transform/public/plugin.ts b/x-pack/plugins/transform/public/plugin.ts
index 03b99ab85ad27..098b55765a889 100644
--- a/x-pack/plugins/transform/public/plugin.ts
+++ b/x-pack/plugins/transform/public/plugin.ts
@@ -24,12 +24,14 @@ import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-manag
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import type { PluginInitializerContext } from '@kbn/core/public';
+import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import { registerFeature } from './register_feature';
import { getTransformHealthRuleType } from './alerting';
export interface PluginsDependencies {
charts: ChartsPluginStart;
data: DataPublicPluginStart;
+ dataViewEditor?: DataViewEditorStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
management: ManagementSetup;
diff --git a/x-pack/plugins/transform/tsconfig.json b/x-pack/plugins/transform/tsconfig.json
index 7b41f101c15c1..d58a875eec9c5 100644
--- a/x-pack/plugins/transform/tsconfig.json
+++ b/x-pack/plugins/transform/tsconfig.json
@@ -68,7 +68,8 @@
"@kbn/ebt-tools",
"@kbn/content-management-plugin",
"@kbn/react-kibana-mount",
- "@kbn/core-plugins-server"
+ "@kbn/core-plugins-server",
+ "@kbn/data-view-editor-plugin"
],
"exclude": [
"target/**/*",
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index ed5d0042c8030..61e5306384a53 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -10776,8 +10776,6 @@
"xpack.cases.getCurrentUser.unknownUser": "Inconnu",
"xpack.cases.header.badge.betaDesc": "Cette fonctionnalité est actuellement en version bêta. Si vous rencontrez des bugs ou si vous souhaitez apporter des commentaires, ouvrez un ticket de problème ou visitez notre forum de discussion.",
"xpack.cases.header.badge.betaLabel": "Bêta",
- "xpack.cases.header.badge.experimentalDesc": "Cette fonctionnalité est en version d'évaluation technique et pourra être modifiée ou retirée complètement dans une future version. Elastic s'efforcera au maximum de corriger tout problème, mais les fonctionnalités en version d'évaluation technique ne sont pas soumises aux accords de niveau de service d'assistance des fonctionnalités officielles en disponibilité générale.",
- "xpack.cases.header.badge.experimentalLabel": "Version d'évaluation technique",
"xpack.cases.header.editableTitle.cancel": "Annuler",
"xpack.cases.header.editableTitle.save": "Enregistrer",
"xpack.cases.markdownEditor.plugins.lens.addVisualizationModalTitle": "Ajouter une visualisation",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 07f770568b038..e5dcbb8b17af5 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -10791,8 +10791,6 @@
"xpack.cases.getCurrentUser.unknownUser": "不明",
"xpack.cases.header.badge.betaDesc": "現在、この機能はベータです。不具合を見つけた場合やご意見がある場合、サポートに問い合わせるか、またはディスカッションフォーラムにご報告ください。",
"xpack.cases.header.badge.betaLabel": "ベータ",
- "xpack.cases.header.badge.experimentalDesc": "この機能はテクニカルプレビュー中であり、将来のリリースでは変更されたり完全に削除されたりする場合があります。Elasticは最善の努力を講じてすべての問題の修正に努めますが、テクニカルプレビュー中の機能には正式なGA機能のサポートSLAが適用されません。",
- "xpack.cases.header.badge.experimentalLabel": "テクニカルプレビュー",
"xpack.cases.header.editableTitle.cancel": "キャンセル",
"xpack.cases.header.editableTitle.save": "保存",
"xpack.cases.markdownEditor.plugins.lens.addVisualizationModalTitle": "ビジュアライゼーションを追加",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 4369612f97b4a..fc12edb97a8e5 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -10791,8 +10791,6 @@
"xpack.cases.getCurrentUser.unknownUser": "未知",
"xpack.cases.header.badge.betaDesc": "此功能当前为公测版。如果遇到任何错误或有任何反馈,请报告问题或访问我们的论坛。",
"xpack.cases.header.badge.betaLabel": "公测版",
- "xpack.cases.header.badge.experimentalDesc": "此功能处于技术预览状态,在未来版本中可能会更改或完全移除。Elastic 将尽最大努力来修复任何问题,但处于技术预览状态的功能不受正式 GA 功能支持 SLA 的约束。",
- "xpack.cases.header.badge.experimentalLabel": "技术预览",
"xpack.cases.header.editableTitle.cancel": "取消",
"xpack.cases.header.editableTitle.save": "保存",
"xpack.cases.markdownEditor.plugins.lens.addVisualizationModalTitle": "添加可视化",
diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts
index 6a17340094e80..dfab65332a9e6 100644
--- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts
+++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts
@@ -163,6 +163,8 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.hits.hits[0]._source).property('event.kind', 'signal');
expect(resp.hits.hits[0]._source).property('event.action', 'open');
+ expect(resp.hits.hits[0]._source).not.have.property('kibana.alert.group');
+
expect(resp.hits.hits[0]._source)
.property('kibana.alert.rule.parameters')
.eql({
diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/group_by_fired.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/group_by_fired.ts
index da18b429c45c0..1129ad40b5203 100644
--- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/group_by_fired.ts
+++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/group_by_fired.ts
@@ -111,7 +111,7 @@ export default function ({ getService }: FtrProviderContext) {
},
index: DATA_VIEW_ID,
},
- groupBy: ['host.name'],
+ groupBy: ['host.name', 'container.id'],
},
actions: [
{
@@ -125,6 +125,7 @@ export default function ({ getService }: FtrProviderContext) {
reason: '{{context.reason}}',
value: '{{context.value}}',
host: '{{context.host}}',
+ group: '{{context.group}}',
},
],
},
@@ -180,7 +181,10 @@ export default function ({ getService }: FtrProviderContext) {
'custom_threshold.fired'
);
expect(resp.hits.hits[0]._source).property('tags').contain('observability');
- expect(resp.hits.hits[0]._source).property('kibana.alert.instance.id', 'host-0');
+ expect(resp.hits.hits[0]._source).property(
+ 'kibana.alert.instance.id',
+ 'host-0,container-0'
+ );
expect(resp.hits.hits[0]._source).property('kibana.alert.workflow_status', 'open');
expect(resp.hits.hits[0]._source).property('event.kind', 'signal');
expect(resp.hits.hits[0]._source).property('event.action', 'open');
@@ -193,6 +197,19 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.hits.hits[0]._source).property('container.name', 'container-name');
expect(resp.hits.hits[0]._source).not.property('container.cpu');
+ expect(resp.hits.hits[0]._source)
+ .property('kibana.alert.group')
+ .eql([
+ {
+ field: 'host.name',
+ value: 'host-0',
+ },
+ {
+ field: 'container.id',
+ value: 'container-0',
+ },
+ ]);
+
expect(resp.hits.hits[0]._source)
.property('kibana.alert.rule.parameters')
.eql({
@@ -209,7 +226,7 @@ export default function ({ getService }: FtrProviderContext) {
alertOnNoData: true,
alertOnGroupDisappear: true,
searchConfiguration: { index: 'data-view-id', query: { query: '', language: 'kuery' } },
- groupBy: ['host.name'],
+ groupBy: ['host.name', 'container.id'],
});
});
@@ -221,6 +238,7 @@ export default function ({ getService }: FtrProviderContext) {
reason: string;
value: string;
host: string;
+ group: string;
}>({
esClient,
indexName: ALERT_ACTION_INDEX,
@@ -231,12 +249,15 @@ export default function ({ getService }: FtrProviderContext) {
`https://localhost:5601/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)`
);
expect(resp.hits.hits[0]._source?.reason).eql(
- 'Custom equation is 0.8 in the last 1 min for host-0. Alert when >= 0.2.'
+ 'Custom equation is 0.8 in the last 1 min for host-0,container-0. Alert when >= 0.2.'
);
expect(resp.hits.hits[0]._source?.value).eql('0.8');
expect(resp.hits.hits[0]._source?.host).eql(
'{"name":"host-0","mac":["00-00-5E-00-53-23","00-00-5E-00-53-24"]}'
);
+ expect(resp.hits.hits[0]._source?.group).eql(
+ '{"field":"host.name","value":"host-0"},{"field":"container.id","value":"container-0"}'
+ );
});
});
});
diff --git a/x-pack/test/cases_api_integration/common/lib/api/configuration.ts b/x-pack/test/cases_api_integration/common/lib/api/configuration.ts
index b0f21f8dc386f..7b1b6d1b76c6c 100644
--- a/x-pack/test/cases_api_integration/common/lib/api/configuration.ts
+++ b/x-pack/test/cases_api_integration/common/lib/api/configuration.ts
@@ -6,10 +6,14 @@
*/
import { CASE_CONFIGURE_URL } from '@kbn/cases-plugin/common/constants';
-import { ConfigurationRequest } from '@kbn/cases-plugin/common/types/api';
+import {
+ ConfigurationPatchRequest,
+ ConfigurationRequest,
+} from '@kbn/cases-plugin/common/types/api';
import {
CaseConnector,
Configuration,
+ Configurations,
ConnectorTypes,
} from '@kbn/cases-plugin/common/types/domain';
import type SuperTest from 'supertest';
@@ -38,6 +42,7 @@ export const getConfigurationRequest = ({
} as CaseConnector,
closure_type: 'close-by-user',
owner: 'securitySolutionFixture',
+ customFields: [],
...overrides,
};
};
@@ -49,6 +54,7 @@ export const getConfigurationOutput = (update = false, overwrite = {}): Partial<
mappings: [],
created_by: { email: null, full_name: null, username: 'elastic' },
updated_by: update ? { email: null, full_name: null, username: 'elastic' } : null,
+ customFields: [],
...overwrite,
};
};
@@ -72,3 +78,45 @@ export const createConfiguration = async (
return configuration;
};
+
+export const getConfiguration = async ({
+ supertest,
+ query = { owner: 'securitySolutionFixture' },
+ expectedHttpCode = 200,
+ auth = { user: superUser, space: null },
+}: {
+ supertest: SuperTest.SuperTest;
+ query?: Record;
+ expectedHttpCode?: number;
+ auth?: { user: User; space: string | null };
+}): Promise => {
+ const { body: configuration } = await supertest
+ .get(`${getSpaceUrlPrefix(auth.space)}${CASE_CONFIGURE_URL}`)
+ .auth(auth.user.username, auth.user.password)
+ .set('kbn-xsrf', 'true')
+ .query(query)
+ .expect(expectedHttpCode);
+
+ return configuration;
+};
+
+export const updateConfiguration = async (
+ supertest: SuperTest.SuperTest,
+ id: string,
+ req: ConfigurationPatchRequest,
+ expectedHttpCode: number = 200,
+ auth: { user: User; space: string | null } | null = { user: superUser, space: null },
+ headers: Record = {}
+): Promise => {
+ const apiCall = supertest.patch(`${getSpaceUrlPrefix(auth?.space)}${CASE_CONFIGURE_URL}/${id}`);
+
+ setupAuth({ apiCall, headers, auth });
+
+ const { body: configuration } = await apiCall
+ .set('kbn-xsrf', 'true')
+ .set(headers)
+ .send(req)
+ .expect(expectedHttpCode);
+
+ return configuration;
+};
diff --git a/x-pack/test/cases_api_integration/common/lib/api/index.ts b/x-pack/test/cases_api_integration/common/lib/api/index.ts
index 6a539457849e6..361a00eadb518 100644
--- a/x-pack/test/cases_api_integration/common/lib/api/index.ts
+++ b/x-pack/test/cases_api_integration/common/lib/api/index.ts
@@ -18,7 +18,6 @@ import {
CASES_URL,
CASE_COMMENT_SAVED_OBJECT,
CASE_CONFIGURE_SAVED_OBJECT,
- CASE_CONFIGURE_URL,
CASE_REPORTERS_URL,
CASE_SAVED_OBJECT,
CASE_STATUS_URL,
@@ -30,13 +29,10 @@ import {
import { CaseMetricsFeature } from '@kbn/cases-plugin/common';
import type { SingleCaseMetricsResponse, CasesMetricsResponse } from '@kbn/cases-plugin/common';
import { SignalHit } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/types';
-import { ActionResult } from '@kbn/actions-plugin/server/types';
import { CasePersistedAttributes } from '@kbn/cases-plugin/server/common/types/case';
import type { SavedObjectsRawDocSource } from '@kbn/core/server';
import type { ConfigurationPersistedAttributes } from '@kbn/cases-plugin/server/common/types/configure';
import {
- Configurations,
- Configuration,
ConnectorMappingsAttributes,
Case,
Cases,
@@ -49,7 +45,6 @@ import {
CasesFindResponse,
CasesPatchRequest,
CasesStatusResponse,
- ConfigurationPatchRequest,
GetRelatedCasesByAlertResponse,
} from '@kbn/cases-plugin/common/types/api';
import { User } from '../authentication/types';
@@ -441,52 +436,6 @@ export const updateCase = async ({
return cases;
};
-export const getConfiguration = async ({
- supertest,
- query = { owner: 'securitySolutionFixture' },
- expectedHttpCode = 200,
- auth = { user: superUser, space: null },
-}: {
- supertest: SuperTest.SuperTest;
- query?: Record;
- expectedHttpCode?: number;
- auth?: { user: User; space: string | null };
-}): Promise => {
- const { body: configuration } = await supertest
- .get(`${getSpaceUrlPrefix(auth.space)}${CASE_CONFIGURE_URL}`)
- .auth(auth.user.username, auth.user.password)
- .set('kbn-xsrf', 'true')
- .query(query)
- .expect(expectedHttpCode);
-
- return configuration;
-};
-
-export type CreateConnectorResponse = Omit & {
- connector_type_id: string;
-};
-
-export const updateConfiguration = async (
- supertest: SuperTest.SuperTest,
- id: string,
- req: ConfigurationPatchRequest,
- expectedHttpCode: number = 200,
- auth: { user: User; space: string | null } | null = { user: superUser, space: null },
- headers: Record = {}
-): Promise => {
- const apiCall = supertest.patch(`${getSpaceUrlPrefix(auth?.space)}${CASE_CONFIGURE_URL}/${id}`);
-
- setupAuth({ apiCall, headers, auth });
-
- const { body: configuration } = await apiCall
- .set('kbn-xsrf', 'true')
- .set(headers)
- .send(req)
- .expect(expectedHttpCode);
-
- return configuration;
-};
-
export const getAllCasesStatuses = async ({
supertest,
expectedHttpCode = 200,
diff --git a/x-pack/test/cases_api_integration/common/lib/mock.ts b/x-pack/test/cases_api_integration/common/lib/mock.ts
index b18ea72e02809..969df10a53d1d 100644
--- a/x-pack/test/cases_api_integration/common/lib/mock.ts
+++ b/x-pack/test/cases_api_integration/common/lib/mock.ts
@@ -174,6 +174,7 @@ export const postCaseResp = (
status: CaseStatuses.open,
updated_by: null,
category: null,
+ customFields: [],
});
interface CommentRequestWithID {
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts
index 3f5d757124e4e..060b6463c0451 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts
@@ -79,7 +79,7 @@ export default ({ getService }: FtrProviderContext): void => {
expect(objects).to.have.length(4);
- const expectedCaseRequest = { ...caseRequest, category: null }; // added default value
+ const expectedCaseRequest = { ...caseRequest, category: null, customFields: [] }; // added default value
expectExportToHaveCaseSavedObject(objects, expectedCaseRequest);
expectExportToHaveUserActions(objects, expectedCaseRequest);
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts
index 0aa6d673ec55c..576fc11cdf51b 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts
@@ -15,6 +15,7 @@ import {
CaseSeverity,
CaseStatuses,
ConnectorTypes,
+ CustomFieldTypes,
} from '@kbn/cases-plugin/common/types/domain';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import {
@@ -37,6 +38,8 @@ import {
calculateDuration,
getCaseUserActions,
removeServerGeneratedPropertiesFromUserAction,
+ createConfiguration,
+ getConfigurationRequest,
} from '../../../../common/lib/api';
import {
createSignalsIndex,
@@ -74,6 +77,21 @@ export default ({ getService }: FtrProviderContext): void => {
describe('happy path', () => {
it('should patch a case', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ ],
+ },
+ })
+ );
const postedCase = await createCase(supertest, postCaseReq);
const patchedCases = await updateCase({
supertest,
@@ -91,12 +109,19 @@ export default ({ getService }: FtrProviderContext): void => {
const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]);
expect(data).to.eql({
...postCaseResp(),
+ customFields: [
+ {
+ key: 'test_custom_field',
+ type: CustomFieldTypes.TEXT,
+ value: null,
+ },
+ ],
title: 'new title',
updated_by: defaultUser,
});
});
- it('should closes the case correctly', async () => {
+ it('should close the case correctly', async () => {
const postedCase = await createCase(supertest, postCaseReq);
const patchedCases = await updateCase({
supertest,
@@ -288,6 +313,136 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
+ it('should patch a case with customFields', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field_1',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ {
+ key: 'test_custom_field_2',
+ label: 'toggle',
+ type: CustomFieldTypes.TOGGLE,
+ required: true,
+ },
+ ],
+ },
+ })
+ );
+
+ const postedCase = await createCase(supertest, {
+ ...postCaseReq,
+ customFields: [
+ {
+ key: 'test_custom_field_2',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ],
+ });
+ const patchedCases = await updateCase({
+ supertest,
+ params: {
+ cases: [
+ {
+ id: postedCase.id,
+ version: postedCase.version,
+ customFields: [
+ {
+ key: 'test_custom_field_1',
+ type: CustomFieldTypes.TEXT,
+ value: ['this is a text field value'],
+ },
+ {
+ key: 'test_custom_field_2',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ],
+ },
+ ],
+ },
+ });
+
+ expect(patchedCases[0].customFields).to.eql([
+ {
+ key: 'test_custom_field_1',
+ type: CustomFieldTypes.TEXT,
+ value: ['this is a text field value'],
+ },
+ {
+ key: 'test_custom_field_2',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ]);
+ });
+
+ it('should fill out missing optional custom fields', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field_1',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ {
+ key: 'test_custom_field_2',
+ label: 'toggle',
+ type: CustomFieldTypes.TOGGLE,
+ required: true,
+ },
+ ],
+ },
+ })
+ );
+
+ const postedCase = await createCase(supertest, {
+ ...postCaseReq,
+ customFields: [
+ {
+ key: 'test_custom_field_2',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ],
+ });
+
+ const patchedCases = await updateCase({
+ supertest,
+ params: {
+ cases: [
+ {
+ id: postedCase.id,
+ version: postedCase.version,
+ customFields: [
+ {
+ key: 'test_custom_field_2',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ],
+ },
+ ],
+ },
+ });
+
+ expect(patchedCases[0].customFields).to.eql([
+ { key: 'test_custom_field_2', type: 'toggle', value: true },
+ { key: 'test_custom_field_1', type: 'text', value: null },
+ ]);
+ });
+
describe('duration', () => {
it('updates the duration correctly when the case closes', async () => {
const postedCase = await createCase(supertest, postCaseReq);
@@ -817,6 +972,158 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
});
+
+ describe('customFields', async () => {
+ it('400s when trying to patch with duplicated custom field keys', async () => {
+ const postedCase = await createCase(supertest, postCaseReq);
+
+ await updateCase({
+ supertest,
+ params: {
+ cases: [
+ {
+ id: postedCase.id,
+ version: postedCase.version,
+ customFields: [
+ {
+ key: 'duplicated_key',
+ type: CustomFieldTypes.TEXT,
+ value: ['this is a text field value'],
+ },
+ {
+ key: 'duplicated_key',
+ type: CustomFieldTypes.TEXT,
+ value: ['this is a text field value'],
+ },
+ ],
+ },
+ ],
+ },
+ expectedHttpCode: 400,
+ });
+ });
+
+ it('400s when trying to patch with a custom field key that does not exist', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ ],
+ },
+ })
+ );
+ const postedCase = await createCase(supertest, postCaseReq);
+
+ await updateCase({
+ supertest,
+ params: {
+ cases: [
+ {
+ id: postedCase.id,
+ version: postedCase.version,
+ customFields: [
+ {
+ key: 'key_does_not_exist',
+ type: CustomFieldTypes.TEXT,
+ value: ['this is a text field value'],
+ },
+ ],
+ },
+ ],
+ },
+ expectedHttpCode: 400,
+ });
+ });
+
+ it('400s when trying to patch a case with a missing required custom field', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: true,
+ },
+ ],
+ },
+ })
+ );
+
+ const postedCase = await createCase(supertest, {
+ ...postCaseReq,
+ customFields: [
+ {
+ key: 'test_custom_field',
+ type: CustomFieldTypes.TEXT,
+ value: ['hello'],
+ },
+ ],
+ });
+
+ await updateCase({
+ supertest,
+ params: {
+ cases: [
+ {
+ id: postedCase.id,
+ version: postedCase.version,
+ customFields: [],
+ },
+ ],
+ },
+ expectedHttpCode: 400,
+ });
+ });
+
+ it('400s when trying to patch a case with a custom field with the wrong type', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ ],
+ },
+ })
+ );
+ const postedCase = await createCase(supertest, postCaseReq);
+
+ await updateCase({
+ supertest,
+ params: {
+ cases: [
+ {
+ id: postedCase.id,
+ version: postedCase.version,
+ customFields: [
+ {
+ key: 'test_custom_field',
+ type: CustomFieldTypes.TOGGLE,
+ value: false,
+ },
+ ],
+ },
+ ],
+ },
+ expectedHttpCode: 400,
+ });
+ });
+ });
});
describe('alerts', () => {
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts
index c32103ea51fef..6d152008e9074 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts
@@ -8,15 +8,21 @@
import expect from '@kbn/expect';
import { CASES_URL } from '@kbn/cases-plugin/common/constants';
-import { CaseStatuses, CaseSeverity } from '@kbn/cases-plugin/common/types/domain';
+import {
+ CaseStatuses,
+ CaseSeverity,
+ CustomFieldTypes,
+} from '@kbn/cases-plugin/common/types/domain';
import { ConnectorJiraTypeFields, ConnectorTypes } from '@kbn/cases-plugin/common/types/domain';
import { getPostCaseRequest, postCaseResp, defaultUser } from '../../../../common/lib/mock';
import {
- deleteCasesByESQuery,
+ deleteAllCaseItems,
createCase,
removeServerGeneratedPropertiesFromCase,
getCaseUserActions,
removeServerGeneratedPropertiesFromUserAction,
+ createConfiguration,
+ getConfigurationRequest,
} from '../../../../common/lib/api';
import {
secOnly,
@@ -37,7 +43,7 @@ export default ({ getService }: FtrProviderContext): void => {
describe('post_case', () => {
afterEach(async () => {
- await deleteCasesByESQuery(es);
+ await deleteAllCaseItems(es);
});
describe('happy path', () => {
@@ -155,6 +161,7 @@ export default ({ getService }: FtrProviderContext): void => {
severity: CaseSeverity.LOW,
assignees: [],
category: null,
+ customFields: [],
},
});
});
@@ -165,6 +172,103 @@ export default ({ getService }: FtrProviderContext): void => {
expect(data).to.eql(postCaseResp());
});
+
+ it('should post a case with customFields', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'valid_key_1',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ {
+ key: 'valid_key_2',
+ label: 'toggle',
+ type: CustomFieldTypes.TOGGLE,
+ required: true,
+ },
+ ],
+ },
+ })
+ );
+
+ const res = await createCase(
+ supertest,
+ getPostCaseRequest({
+ customFields: [
+ {
+ key: 'valid_key_1',
+ type: CustomFieldTypes.TEXT,
+ value: ['this is a text field value'],
+ },
+ {
+ key: 'valid_key_2',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ],
+ })
+ );
+
+ expect(res.customFields).to.eql([
+ {
+ key: 'valid_key_1',
+ type: CustomFieldTypes.TEXT,
+ value: ['this is a text field value'],
+ },
+ {
+ key: 'valid_key_2',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ]);
+ });
+
+ it('should fill out missing custom fields', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'valid_key_1',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ {
+ key: 'valid_key_2',
+ label: 'toggle',
+ type: CustomFieldTypes.TOGGLE,
+ required: true,
+ },
+ ],
+ },
+ })
+ );
+
+ const res = await createCase(
+ supertest,
+ getPostCaseRequest({
+ customFields: [
+ {
+ key: 'valid_key_2',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ],
+ })
+ );
+
+ expect(res.customFields).to.eql([
+ { key: 'valid_key_2', type: 'toggle', value: true },
+ { key: 'valid_key_1', type: 'text', value: null },
+ ]);
+ });
});
describe('unhappy path', () => {
@@ -324,6 +428,128 @@ export default ({ getService }: FtrProviderContext): void => {
);
});
});
+
+ describe('customFields', async () => {
+ it('400s when trying to patch with duplicated custom field keys', async () => {
+ await createCase(
+ supertest,
+ getPostCaseRequest({
+ customFields: [
+ {
+ key: 'duplicated_key',
+ type: CustomFieldTypes.TEXT,
+ value: ['this is a text field value'],
+ },
+ {
+ key: 'duplicated_key',
+ type: CustomFieldTypes.TEXT,
+ value: ['this is a text field value'],
+ },
+ ],
+ }),
+ 400
+ );
+ });
+
+ it('400s when trying to create case with customField key that does not exist', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ ],
+ },
+ })
+ );
+ await createCase(
+ supertest,
+ getPostCaseRequest({
+ customFields: [
+ {
+ key: 'invalid_key',
+ type: CustomFieldTypes.TEXT,
+ value: ['this is a text field value'],
+ },
+ ],
+ }),
+ 400
+ );
+ });
+
+ it('400s when trying to create case with a required custom field', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ {
+ key: 'toggle_custom_field',
+ label: 'toggle',
+ type: CustomFieldTypes.TOGGLE,
+ required: true,
+ },
+ ],
+ },
+ })
+ );
+ await createCase(
+ supertest,
+ getPostCaseRequest({
+ customFields: [
+ {
+ key: 'test_custom_field',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ],
+ }),
+ 400
+ );
+ });
+
+ it('400s when trying to create case with a custom field with the wrong type', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: true,
+ },
+ ],
+ },
+ })
+ );
+ await createCase(
+ supertest,
+ getPostCaseRequest({
+ customFields: [
+ {
+ key: 'test_custom_field',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ ],
+ }),
+ 400
+ );
+ });
+ });
});
describe('rbac', () => {
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/resolve_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/resolve_case.ts
index 042b9856fc45c..7679a3fbda9a7 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/resolve_case.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/resolve_case.ts
@@ -18,7 +18,7 @@ import {
getPostCaseRequest,
} from '../../../../common/lib/mock';
import {
- deleteCasesByESQuery,
+ deleteAllCaseItems,
createCase,
resolveCase,
createComment,
@@ -46,7 +46,7 @@ export default ({ getService }: FtrProviderContext): void => {
describe('resolve_case', () => {
afterEach(async () => {
- await deleteCasesByESQuery(es);
+ await deleteAllCaseItems(es);
});
it('should resolve a case with no comments', async () => {
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/get_configure.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/get_configure.ts
index 94e414fa23f45..87c65ba8a47c9 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/get_configure.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/get_configure.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { CustomFieldTypes } from '@kbn/cases-plugin/common/types/domain';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
@@ -47,12 +48,32 @@ export default ({ getService }: FtrProviderContext): void => {
it('should return a configuration', async () => {
await createConfiguration(supertest);
- const configuration = await getConfiguration({ supertest });
+ const configuration = await getConfiguration({ supertest });
const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]);
+
expect(data).to.eql(getConfigurationOutput());
});
+ it('should return a configuration with customFields', async () => {
+ const customFields = {
+ customFields: [
+ { key: 'hello', label: 'text', type: CustomFieldTypes.TEXT, required: false },
+ { key: 'goodbye', label: 'toggle', type: CustomFieldTypes.TOGGLE, required: true },
+ ],
+ };
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: customFields,
+ })
+ );
+ const configuration = await getConfiguration({ supertest });
+
+ const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]);
+ expect(data).to.eql(getConfigurationOutput(false, customFields));
+ });
+
it('should get a single configuration', async () => {
await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' }));
await createConfiguration(supertest);
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts
index d94ff433550e7..407529738b488 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts
@@ -6,7 +6,7 @@
*/
import expect from '@kbn/expect';
-import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain';
+import { ConnectorTypes, CustomFieldTypes } from '@kbn/cases-plugin/common/types/domain';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib';
@@ -54,6 +54,31 @@ export default ({ getService }: FtrProviderContext): void => {
expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' });
});
+ it('should patch a configuration with customFields', async () => {
+ const customFields = [
+ {
+ key: 'text_field',
+ label: '#1',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ {
+ key: 'toggle_field',
+ label: '#2',
+ type: CustomFieldTypes.TOGGLE,
+ required: false,
+ },
+ ];
+ const configuration = await createConfiguration(supertest);
+ const newConfiguration = await updateConfiguration(supertest, configuration.id, {
+ version: configuration.version,
+ customFields,
+ });
+
+ const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration);
+ expect(data).to.eql({ ...getConfigurationOutput(true), customFields });
+ });
+
it('should update mapping when changing connector', async () => {
const configuration = await createConfiguration(supertest);
await updateConfiguration(supertest, configuration.id, {
@@ -87,80 +112,149 @@ export default ({ getService }: FtrProviderContext): void => {
]);
});
- it('should not patch a configuration with unsupported connector type', async () => {
- const configuration = await createConfiguration(supertest);
- await updateConfiguration(
- supertest,
- configuration.id,
- // @ts-expect-error
- getConfigurationRequest({ type: '.unsupported' }),
- 400
- );
- });
+ describe('validation', () => {
+ it('should not patch a configuration with unsupported connector type', async () => {
+ const configuration = await createConfiguration(supertest);
+ await updateConfiguration(
+ supertest,
+ configuration.id,
+ // @ts-expect-error
+ getConfigurationRequest({ type: '.unsupported' }),
+ 400
+ );
+ });
- it('should not patch a configuration with unsupported connector fields', async () => {
- const configuration = await createConfiguration(supertest);
- await updateConfiguration(
- supertest,
- configuration.id,
- // @ts-expect-error
- getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }),
- 400
- );
- });
+ it('should not patch a configuration with unsupported connector fields', async () => {
+ const configuration = await createConfiguration(supertest);
+ await updateConfiguration(
+ supertest,
+ configuration.id,
+ // @ts-expect-error
+ getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }),
+ 400
+ );
+ });
+
+ it('should handle patch request when there is no configuration', async () => {
+ const error = await updateConfiguration(
+ supertest,
+ 'not-exist',
+ { closure_type: 'close-by-pushing', version: 'no-version' },
+ 404
+ );
- it('should handle patch request when there is no configuration', async () => {
- const error = await updateConfiguration(
- supertest,
- 'not-exist',
- { closure_type: 'close-by-pushing', version: 'no-version' },
- 404
- );
-
- expect(error).to.eql({
- error: 'Not Found',
- message: 'Saved object [cases-configure/not-exist] not found',
- statusCode: 404,
+ expect(error).to.eql({
+ error: 'Not Found',
+ message: 'Saved object [cases-configure/not-exist] not found',
+ statusCode: 404,
+ });
});
- });
- it('should handle patch request when versions are different', async () => {
- const configuration = await createConfiguration(supertest);
- const error = await updateConfiguration(
- supertest,
- configuration.id,
- { closure_type: 'close-by-pushing', version: 'no-version' },
- 409
- );
-
- expect(error).to.eql({
- error: 'Conflict',
- message:
- 'This configuration has been updated. Please refresh before saving additional updates.',
- statusCode: 409,
+ it('should handle patch request when versions are different', async () => {
+ const configuration = await createConfiguration(supertest);
+ const error = await updateConfiguration(
+ supertest,
+ configuration.id,
+ { closure_type: 'close-by-pushing', version: 'no-version' },
+ 409
+ );
+
+ expect(error).to.eql({
+ error: 'Conflict',
+ message:
+ 'This configuration has been updated. Please refresh before saving additional updates.',
+ statusCode: 409,
+ });
});
- });
- it('should not allow to change the owner of the configuration', async () => {
- const configuration = await createConfiguration(supertest);
- await updateConfiguration(
- supertest,
- configuration.id,
- // @ts-expect-error
- { owner: 'observabilityFixture', version: configuration.version },
- 400
- );
- });
+ it('should not allow to change the owner of the configuration', async () => {
+ const configuration = await createConfiguration(supertest);
+ await updateConfiguration(
+ supertest,
+ configuration.id,
+ // @ts-expect-error
+ { owner: 'observabilityFixture', version: configuration.version },
+ 400
+ );
+ });
- it('should not allow excess attributes', async () => {
- const configuration = await createConfiguration(supertest);
- await updateConfiguration(
- supertest,
- configuration.id,
- // @ts-expect-error
- { notExist: 'not-exist', version: configuration.version },
- 400
- );
+ it('should not allow excess attributes', async () => {
+ const configuration = await createConfiguration(supertest);
+ await updateConfiguration(
+ supertest,
+ configuration.id,
+ // @ts-expect-error
+ { notExist: 'not-exist', version: configuration.version },
+ 400
+ );
+ });
+
+ it('should not allow patching the type of a custom field', async () => {
+ const configuration = await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'wrong_type_key',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ ],
+ },
+ })
+ );
+
+ await updateConfiguration(
+ supertest,
+ configuration.id,
+ {
+ version: configuration.version,
+ customFields: [
+ {
+ key: 'wrong_type_key',
+ label: '#1',
+ type: CustomFieldTypes.TOGGLE,
+ required: false,
+ },
+ ],
+ },
+ 400
+ );
+ });
+
+ it('should not patch a configuration with duplicated custom field keys', async () => {
+ const configuration = await createConfiguration(supertest);
+ await updateConfiguration(
+ supertest,
+ configuration.id,
+ {
+ version: configuration.version,
+ customFields: [
+ {
+ key: 'triplicated_key',
+ label: '#1',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ {
+ key: 'triplicated_key',
+ label: '#2',
+ type: CustomFieldTypes.TOGGLE,
+ required: false,
+ },
+ {
+ key: 'triplicated_key',
+ label: '#2',
+ type: CustomFieldTypes.TOGGLE,
+ required: false,
+ },
+ ],
+ },
+ 400
+ );
+ });
});
describe('rbac', () => {
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts
index 0b152d5cd9d95..02721521a2e4f 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/configure/post_configure.ts
@@ -6,7 +6,8 @@
*/
import expect from '@kbn/expect';
-import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain';
+import { ConnectorTypes, CustomFieldTypes } from '@kbn/cases-plugin/common/types/domain';
+import { MAX_CUSTOM_FIELD_LABEL_LENGTH } from '@kbn/cases-plugin/common/constants';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib';
@@ -52,6 +53,32 @@ export default ({ getService }: FtrProviderContext): void => {
expect(data).to.eql(getConfigurationOutput());
});
+ it('should create a configuration with no customFields', async () => {
+ const { customFields, ...configurationRequest } = getConfigurationRequest();
+ const configuration = await createConfiguration(supertest, configurationRequest);
+
+ expect(configuration.customFields).to.eql([]);
+ });
+
+ it('should create a configuration with customFields', async () => {
+ const customFields = {
+ customFields: [
+ { key: 'hello', label: 'text', type: CustomFieldTypes.TEXT, required: false },
+ { key: 'goodbye', label: 'toggle', type: CustomFieldTypes.TOGGLE, required: true },
+ ],
+ };
+
+ const configuration = await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: customFields,
+ })
+ );
+
+ const data = removeServerGeneratedPropertiesFromSavedObject(configuration);
+ expect(data).to.eql(getConfigurationOutput(false, customFields));
+ });
+
it('should keep only the latest configuration', async () => {
await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' }));
await createConfiguration(supertest);
@@ -194,126 +221,176 @@ export default ({ getService }: FtrProviderContext): void => {
]);
});
- it('should not create a configuration when missing connector.id', async () => {
- await createConfiguration(
- supertest,
- {
- // @ts-expect-error
- connector: {
- name: 'Connector',
- type: ConnectorTypes.none,
- fields: null,
+ describe('validation', () => {
+ it('should not create a configuration when missing connector.id', async () => {
+ await createConfiguration(
+ supertest,
+ {
+ // @ts-expect-error
+ connector: {
+ name: 'Connector',
+ type: ConnectorTypes.none,
+ fields: null,
+ },
+ closure_type: 'close-by-user',
},
- closure_type: 'close-by-user',
- },
- 400
- );
- });
+ 400
+ );
+ });
- it('should not create a configuration when missing connector.name', async () => {
- await createConfiguration(
- supertest,
- {
- // @ts-expect-error
- connector: {
- id: 'test-id',
- type: ConnectorTypes.none,
- fields: null,
+ it('should not create a configuration when missing connector.name', async () => {
+ await createConfiguration(
+ supertest,
+ {
+ // @ts-expect-error
+ connector: {
+ id: 'test-id',
+ type: ConnectorTypes.none,
+ fields: null,
+ },
+ closure_type: 'close-by-user',
},
- closure_type: 'close-by-user',
- },
- 400
- );
- });
+ 400
+ );
+ });
- it('should not create a configuration when missing connector.type', async () => {
- await createConfiguration(
- supertest,
- {
+ it('should not create a configuration when missing connector.type', async () => {
+ await createConfiguration(
+ supertest,
+ {
+ // @ts-expect-error
+ connector: {
+ id: 'test-id',
+ name: 'Connector',
+ fields: null,
+ },
+ closure_type: 'close-by-user',
+ },
+ 400
+ );
+ });
+
+ it('should not create a configuration when missing connector.fields', async () => {
+ await createConfiguration(
+ supertest,
+ {
+ // @ts-expect-error
+ connector: {
+ id: 'test-id',
+ type: ConnectorTypes.none,
+ name: 'Connector',
+ },
+ closure_type: 'close-by-user',
+ },
+ 400
+ );
+ });
+
+ it('should not create a configuration when when missing closure_type', async () => {
+ await createConfiguration(
+ supertest,
// @ts-expect-error
- connector: {
- id: 'test-id',
- name: 'Connector',
- fields: null,
+ {
+ connector: {
+ id: 'test-id',
+ type: ConnectorTypes.none,
+ name: 'Connector',
+ fields: null,
+ },
},
- closure_type: 'close-by-user',
- },
- 400
- );
- });
+ 400
+ );
+ });
- it('should not create a configuration when missing connector.fields', async () => {
- await createConfiguration(
- supertest,
- {
+ it('should not create a configuration when missing connector', async () => {
+ await createConfiguration(
+ supertest,
// @ts-expect-error
- connector: {
- id: 'test-id',
- type: ConnectorTypes.none,
- name: 'Connector',
+ {
+ closure_type: 'close-by-user',
},
- closure_type: 'close-by-user',
- },
- 400
- );
- });
+ 400
+ );
+ });
- it('should not create a configuration when when missing closure_type', async () => {
- await createConfiguration(
- supertest,
- // @ts-expect-error
- {
- connector: {
- id: 'test-id',
- type: ConnectorTypes.none,
- name: 'Connector',
- fields: null,
+ it('should not create a configuration when fields are not null', async () => {
+ await createConfiguration(
+ supertest,
+ {
+ connector: {
+ id: 'test-id',
+ type: ConnectorTypes.none,
+ name: 'Connector',
+ // @ts-expect-error
+ fields: {},
+ },
+ closure_type: 'close-by-user',
},
- },
- 400
- );
- });
+ 400
+ );
+ });
- it('should not create a configuration when missing connector', async () => {
- await createConfiguration(
- supertest,
- // @ts-expect-error
- {
- closure_type: 'close-by-user',
- },
- 400
- );
- });
+ it('should not create a configuration with unsupported connector type', async () => {
+ await createConfiguration(
+ supertest,
+ // @ts-expect-error
+ getConfigurationRequest({ type: '.unsupported' }),
+ 400
+ );
+ });
- it('should not create a configuration when fields are not null', async () => {
- await createConfiguration(
- supertest,
- {
- connector: {
- id: 'test-id',
- type: ConnectorTypes.none,
- name: 'Connector',
- // @ts-expect-error
- fields: {},
- },
- closure_type: 'close-by-user',
- },
- 400
- );
- });
+ it('should not create a configuration with unsupported connector fields', async () => {
+ await createConfiguration(
+ supertest,
+ // @ts-expect-error
+ getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }),
+ 400
+ );
+ });
- it('should not create a configuration with unsupported connector type', async () => {
- // @ts-expect-error
- await createConfiguration(supertest, getConfigurationRequest({ type: '.unsupported' }), 400);
- });
+ it('should not create a configuration when customField label is too long', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'hello',
+ label: '#'.repeat(MAX_CUSTOM_FIELD_LABEL_LENGTH + 1),
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ ],
+ },
+ }),
+ 400
+ );
+ });
- it('should not create a configuration with unsupported connector fields', async () => {
- await createConfiguration(
- supertest,
- // @ts-expect-error
- getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }),
- 400
- );
+ it('should not create a configuration with duplicated keys', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'duplicated_key',
+ label: '#1',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ {
+ key: 'duplicated_key',
+ label: '#2',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ ],
+ },
+ }),
+ 400
+ );
+ });
});
describe('rbac', () => {
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts
index 4679a03b4d8af..f5a95fe33d917 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts
@@ -15,6 +15,8 @@ import {
AttachmentType,
CreateCaseUserAction,
ConnectorTypes,
+ CustomFieldTypes,
+ CaseCustomFields,
} from '@kbn/cases-plugin/common/types/domain';
import { getCaseUserActionUrl } from '@kbn/cases-plugin/common/api';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
@@ -30,6 +32,8 @@ import {
deleteComment,
extractWarningValueFromWarningHeader,
getCaseUserActions,
+ createConfiguration,
+ getConfigurationRequest,
} from '../../../../common/lib/api';
import {
globalRead,
@@ -84,6 +88,10 @@ export default ({ getService }: FtrProviderContext): void => {
expect(createCaseUserAction.payload.settings).to.eql(postCaseReq.settings);
expect(createCaseUserAction.payload.owner).to.eql(postCaseReq.owner);
expect(createCaseUserAction.payload.connector).to.eql(postCaseReq.connector);
+ expect(createCaseUserAction.payload.assignees).to.eql(postCaseReq.assignees);
+ expect(createCaseUserAction.payload.severity).to.eql(postCaseReq.severity);
+ expect(createCaseUserAction.payload.category).to.eql(null);
+ expect(createCaseUserAction.payload.customFields).to.eql([]);
});
it('deletes all user actions when a case is deleted', async () => {
@@ -252,12 +260,12 @@ export default ({ getService }: FtrProviderContext): void => {
});
const userActions = await getCaseUserActions({ supertest, caseID: theCase.id });
- const titleUserAction = userActions[1];
+ const descUserAction = userActions[1];
expect(userActions.length).to.eql(2);
- expect(titleUserAction.type).to.eql('description');
- expect(titleUserAction.action).to.eql('update');
- expect(titleUserAction.payload).to.eql({ description: newDesc });
+ expect(descUserAction.type).to.eql('description');
+ expect(descUserAction.action).to.eql('update');
+ expect(descUserAction.payload).to.eql({ description: newDesc });
});
it('creates a create comment user action', async () => {
@@ -349,6 +357,111 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
+ it('creates user actions for custom fields correctly', async () => {
+ await createConfiguration(
+ supertest,
+ getConfigurationRequest({
+ overrides: {
+ customFields: [
+ {
+ key: 'test_custom_field_1',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ {
+ key: 'test_custom_field_2',
+ label: 'toggle',
+ type: CustomFieldTypes.TOGGLE,
+ required: false,
+ },
+ {
+ key: 'test_custom_field_3',
+ label: 'text',
+ type: CustomFieldTypes.TEXT,
+ required: false,
+ },
+ ],
+ },
+ })
+ );
+
+ const customFields: CaseCustomFields = [
+ {
+ key: 'test_custom_field_1',
+ type: CustomFieldTypes.TEXT,
+ value: ['this is a text field value'],
+ },
+ {
+ key: 'test_custom_field_2',
+ type: CustomFieldTypes.TOGGLE,
+ value: true,
+ },
+ {
+ key: 'test_custom_field_3',
+ type: CustomFieldTypes.TEXT,
+ value: ['this is a text field value 3'],
+ },
+ ];
+
+ const theCase = await createCase(supertest, {
+ ...postCaseReq,
+ customFields: [customFields[0], customFields[2]],
+ });
+
+ await updateCase({
+ supertest,
+ params: {
+ cases: [
+ {
+ id: theCase.id,
+ version: theCase.version,
+ customFields: [
+ {
+ key: 'test_custom_field_1',
+ type: CustomFieldTypes.TEXT,
+ value: ['new value'],
+ },
+ ],
+ },
+ ],
+ },
+ });
+
+ const userActions = await getCaseUserActions({ supertest, caseID: theCase.id });
+ expect(userActions.length).to.eql(3);
+
+ const createCaseUserAction = userActions[0] as unknown as CreateCaseUserAction;
+ const updateCustomFieldCaseUserAction = userActions[1];
+ const updateCustomFieldCaseUserAction2 = userActions[2];
+
+ expect(createCaseUserAction.payload.customFields).to.eql([customFields[0], customFields[2]]);
+
+ expect(updateCustomFieldCaseUserAction.type).to.eql('customFields');
+ expect(updateCustomFieldCaseUserAction.action).to.eql('update');
+ expect(updateCustomFieldCaseUserAction.payload).to.eql({
+ customFields: [
+ {
+ key: 'test_custom_field_1',
+ type: CustomFieldTypes.TEXT,
+ value: ['new value'],
+ },
+ ],
+ });
+
+ expect(updateCustomFieldCaseUserAction2.type).to.eql('customFields');
+ expect(updateCustomFieldCaseUserAction2.action).to.eql('update');
+ expect(updateCustomFieldCaseUserAction2.payload).to.eql({
+ customFields: [
+ {
+ key: 'test_custom_field_3',
+ type: CustomFieldTypes.TEXT,
+ value: null,
+ },
+ ],
+ });
+ });
+
describe('rbac', () => {
const supertestWithoutAuth = getService('supertestWithoutAuth');
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/timestamps.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/timestamps.ts
index 9b56ead0d91a6..ef27b7c7ff4bd 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/timestamps.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/timestamps.ts
@@ -7,7 +7,7 @@
import expect from '@kbn/expect';
import { orderBy } from 'lodash';
-import { RuleExecutionStatus } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
import {
EqlRuleCreateProps,
QueryRuleCreateProps,
@@ -261,7 +261,7 @@ export default ({ getService }: FtrProviderContext) => {
log,
es,
createdRule,
- RuleExecutionStatus['partial failure']
+ RuleExecutionStatusEnum['partial failure']
);
expect(signalsOpen.hits.hits.length).eql(0);
});
@@ -340,7 +340,7 @@ export default ({ getService }: FtrProviderContext) => {
log,
es,
createdRule,
- RuleExecutionStatus['partial failure']
+ RuleExecutionStatusEnum['partial failure']
);
expect(signalsOpen.hits.hits.length).eql(0);
});
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts
index 81af34c6c34ac..b2033fed23ed4 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/query.ts
@@ -28,7 +28,7 @@ import {
QueryRuleCreateProps,
AlertSuppressionMissingFieldsStrategy,
} from '@kbn/security-solution-plugin/common/api/detection_engine';
-import { RuleExecutionStatus } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
import { Ancestor } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/types';
import {
ALERT_ANCESTORS,
@@ -797,7 +797,7 @@ export default ({ getService }: FtrProviderContext) => {
log,
es,
createdRule,
- RuleExecutionStatus.succeeded,
+ RuleExecutionStatusEnum.succeeded,
undefined,
afterTimestamp
);
@@ -873,7 +873,7 @@ export default ({ getService }: FtrProviderContext) => {
log,
es,
createdRule,
- RuleExecutionStatus.succeeded,
+ RuleExecutionStatusEnum.succeeded,
undefined,
afterTimestamp
);
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts
index 0638765283a6e..efed9c26375e2 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts
@@ -34,7 +34,7 @@ import {
ALERT_ORIGINAL_EVENT_MODULE,
ALERT_ORIGINAL_TIME,
} from '@kbn/security-solution-plugin/common/field_maps/field_names';
-import { RuleExecutionStatus } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
+import { RuleExecutionStatusEnum } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
import { getMaxSignalsWarning } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/utils';
import {
previewRule,
@@ -173,7 +173,7 @@ export default ({ getService }: FtrProviderContext) => {
log,
es,
createdRule,
- RuleExecutionStatus.succeeded,
+ RuleExecutionStatusEnum.succeeded,
100
);
expect(alerts.hits.hits.length).equal(88);
@@ -354,7 +354,7 @@ export default ({ getService }: FtrProviderContext) => {
log,
es,
createdRule,
- RuleExecutionStatus.succeeded,
+ RuleExecutionStatusEnum.succeeded,
100
);
expect(alerts.hits.hits.length).equal(88);
@@ -557,7 +557,7 @@ export default ({ getService }: FtrProviderContext) => {
log,
es,
createdRuleTerm,
- RuleExecutionStatus.succeeded,
+ RuleExecutionStatusEnum.succeeded,
100
);
const alertsMatch = await getOpenSignals(
@@ -565,7 +565,7 @@ export default ({ getService }: FtrProviderContext) => {
log,
es,
createdRuleMatch,
- RuleExecutionStatus.succeeded,
+ RuleExecutionStatusEnum.succeeded,
100
);
diff --git a/x-pack/test/detection_engine_api_integration/utils/get_open_signals.ts b/x-pack/test/detection_engine_api_integration/utils/get_open_signals.ts
index 817389ba20547..ac44acef91f06 100644
--- a/x-pack/test/detection_engine_api_integration/utils/get_open_signals.ts
+++ b/x-pack/test/detection_engine_api_integration/utils/get_open_signals.ts
@@ -8,7 +8,10 @@
import type SuperTest from 'supertest';
import type { Client } from '@elastic/elasticsearch';
import type { ToolingLog } from '@kbn/tooling-log';
-import { RuleExecutionStatus } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
+import {
+ RuleExecutionStatus,
+ RuleExecutionStatusEnum,
+} from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
import type { RuleResponse } from '@kbn/security-solution-plugin/common/api/detection_engine';
import { waitForRuleStatus } from './wait_for_rule_status';
@@ -20,7 +23,7 @@ export const getOpenSignals = async (
log: ToolingLog,
es: Client,
rule: RuleResponse,
- status: RuleExecutionStatus = RuleExecutionStatus.succeeded,
+ status: RuleExecutionStatus = RuleExecutionStatusEnum.succeeded,
size?: number,
afterDate?: Date
) => {
diff --git a/x-pack/test/detection_engine_api_integration/utils/wait_for_rule_status.ts b/x-pack/test/detection_engine_api_integration/utils/wait_for_rule_status.ts
index 53393fa58db70..59607eeb47d45 100644
--- a/x-pack/test/detection_engine_api_integration/utils/wait_for_rule_status.ts
+++ b/x-pack/test/detection_engine_api_integration/utils/wait_for_rule_status.ts
@@ -8,7 +8,10 @@
import type { ToolingLog } from '@kbn/tooling-log';
import type SuperTest from 'supertest';
import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants';
-import { RuleExecutionStatus } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
+import {
+ RuleExecutionStatus,
+ RuleExecutionStatusEnum,
+} from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring';
import { waitFor } from './wait_for';
import { routeWithNamespace } from './route_with_namespace';
@@ -70,10 +73,10 @@ export const waitForRuleStatus = async (
};
export const waitForRuleSuccess = (params: WaitForRuleStatusParams): Promise =>
- waitForRuleStatus(RuleExecutionStatus.succeeded, params);
+ waitForRuleStatus(RuleExecutionStatusEnum.succeeded, params);
export const waitForRulePartialFailure = (params: WaitForRuleStatusParams): Promise =>
- waitForRuleStatus(RuleExecutionStatus['partial failure'], params);
+ waitForRuleStatus(RuleExecutionStatusEnum['partial failure'], params);
export const waitForRuleFailure = (params: WaitForRuleStatusParams): Promise =>
- waitForRuleStatus(RuleExecutionStatus.failed, params);
+ waitForRuleStatus(RuleExecutionStatusEnum.failed, params);
diff --git a/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts b/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts
index 4ad82d75346bd..67f3728eab9f9 100644
--- a/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts
+++ b/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts
@@ -17,9 +17,7 @@ export default function ({ getService }: FtrProviderContext) {
id: model.name,
}));
- // Failing: See https://github.com/elastic/kibana/issues/165083
- // Failing: See https://github.com/elastic/kibana/issues/165084
- describe.skip('trained models', function () {
+ describe('trained models', function () {
// 'Created at' will be different on each run,
// so we will just assert that the value is in the expected timestamp format.
const builtInModelData = {
diff --git a/x-pack/test/functional/services/cases/common.ts b/x-pack/test/functional/services/cases/common.ts
index ef7964935ae41..7a969b310f56f 100644
--- a/x-pack/test/functional/services/cases/common.ts
+++ b/x-pack/test/functional/services/cases/common.ts
@@ -63,6 +63,7 @@ export function CasesCommonServiceProvider({ getService, getPageObject }: FtrPro
const radioGroup = await testSubjects.find(testSubject);
const label = await radioGroup.findByCssSelector(`label[for="${value}"]`);
await label.click();
+ await header.waitUntilLoadingHasFinished();
await this.assertRadioGroupValue(testSubject, value);
},
diff --git a/x-pack/test/functional/services/cases/list.ts b/x-pack/test/functional/services/cases/list.ts
index ceebee7a2dde1..e1b4e45724283 100644
--- a/x-pack/test/functional/services/cases/list.ts
+++ b/x-pack/test/functional/services/cases/list.ts
@@ -93,7 +93,7 @@ export function CasesTableServiceProvider(
async waitForCasesToBeListed() {
await retry.waitFor('cases to appear on the all cases table', async () => {
- this.refreshTable();
+ await this.refreshTable();
return await testSubjects.exists('case-details-link');
});
await header.waitUntilLoadingHasFinished();
@@ -101,7 +101,7 @@ export function CasesTableServiceProvider(
async waitForCasesToBeDeleted() {
await retry.waitFor('the cases table to be empty', async () => {
- this.refreshTable();
+ await this.refreshTable();
const rows = await find.allByCssSelector('[data-test-subj*="cases-table-row-"', 100);
return rows.length === 0;
});
diff --git a/x-pack/test/functional/services/ml/deploy_models_flyout.ts b/x-pack/test/functional/services/ml/deploy_models_flyout.ts
index ed52ce97c21f5..22e9c87414f36 100644
--- a/x-pack/test/functional/services/ml/deploy_models_flyout.ts
+++ b/x-pack/test/functional/services/ml/deploy_models_flyout.ts
@@ -109,6 +109,9 @@ export function DeployDFAModelFlyoutProvider(
await editor.click();
const input = await find.activeElement();
await input.clearValueWithKeyboard();
+ // Ensure the editor is cleared before adding input
+ const editorContentAfterClearing = await input.getAttribute('value');
+ expect(editorContentAfterClearing).to.eql('');
for (const chr of value) {
await retry.tryForTime(5000, async () => {
@@ -138,6 +141,7 @@ export function DeployDFAModelFlyoutProvider(
'mlTrainedModelsInferencePipelineFieldMapEditButton'
);
await editFieldMapButton.click();
+
await this.setTrainedModelsInferenceFlyoutCustomEditorValues(
'mlTrainedModelsInferencePipelineFieldMapEdit',
JSON.stringify(values.editedFieldMap)
diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts
index b812bbaeb019b..098297551347d 100644
--- a/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts
@@ -13,6 +13,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const cases = getService('cases');
const toasts = getService('toasts');
+ const header = getPageObject('header');
describe('Configure', function () {
before(async () => {
@@ -24,6 +25,9 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
});
describe('Closure options', function () {
+ this.beforeEach(async () => {
+ await header.waitUntilLoadingHasFinished();
+ });
it('defaults the closure option correctly', async () => {
await cases.common.assertRadioGroupValue('closure-options-radio-group', 'close-by-user');
});
@@ -33,6 +37,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
const toast = await toasts.getToastElement(1);
expect(await toast.getVisibleText()).to.be('Saved external connection settings');
await toasts.dismissAllToasts();
+ await cases.common.assertRadioGroupValue('closure-options-radio-group', 'close-by-pushing');
});
});
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/connectors.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/connectors.cy.ts
index d61b24d67c223..ff5b776743841 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/connectors.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/connectors.cy.ts
@@ -10,7 +10,7 @@ import { getServiceNowConnector, getServiceNowITSMHealthResponse } from '../../.
import { SERVICE_NOW_MAPPING } from '../../../screens/configure_cases';
import { goToEditExternalConnection } from '../../../tasks/all_cases';
-import { cleanKibana, deleteCases, deleteConnectors } from '../../../tasks/common';
+import { cleanKibana, deleteAllCasesItems, deleteConnectors } from '../../../tasks/common';
import {
addServiceNowConnector,
openAddNewConnectorOption,
@@ -35,6 +35,7 @@ describe('Cases connectors', { tags: ['@ess', '@serverless'] }, () => {
error: null,
updated_at: null,
updated_by: null,
+ customFields: [],
mappings: [
{ source: 'title', target: 'short_description', action_type: 'overwrite' },
{ source: 'description', target: 'description', action_type: 'overwrite' },
@@ -53,7 +54,7 @@ describe('Cases connectors', { tags: ['@ess', '@serverless'] }, () => {
beforeEach(() => {
login();
- deleteCases();
+ deleteAllCasesItems();
cy.intercept('GET', `${snConnector.URL}/api/x_elas2_inc_int/elastic_api/health*`, {
statusCode: 200,
body: getServiceNowITSMHealthResponse(),
diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/privileges.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/privileges.cy.ts
index 7db79bd446777..73836e1cd913a 100644
--- a/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/privileges.cy.ts
+++ b/x-pack/test/security_solution_cypress/cypress/e2e/explore/cases/privileges.cy.ts
@@ -9,7 +9,7 @@ import type { TestCaseWithoutTimeline } from '../../../objects/case';
import { ALL_CASES_CREATE_NEW_CASE_BTN, ALL_CASES_NAME } from '../../../screens/all_cases';
import { goToCreateNewCase } from '../../../tasks/all_cases';
-import { cleanKibana, deleteCases } from '../../../tasks/common';
+import { cleanKibana, deleteAllCasesItems } from '../../../tasks/common';
import {
backToCases,
@@ -61,7 +61,7 @@ describe('Cases privileges', { tags: ['@ess', '@serverless', '@brokenInServerles
beforeEach(() => {
login();
- deleteCases();
+ deleteAllCasesItems();
});
for (const user of [secAllUser, secReadCasesAllUser, secAllCasesNoDeleteUser]) {
diff --git a/x-pack/test/security_solution_cypress/cypress/objects/case.ts b/x-pack/test/security_solution_cypress/cypress/objects/case.ts
index 2abdce5fb068c..f30f24fb6ff06 100644
--- a/x-pack/test/security_solution_cypress/cypress/objects/case.ts
+++ b/x-pack/test/security_solution_cypress/cypress/objects/case.ts
@@ -74,6 +74,7 @@ export const getCaseResponse = (): Case => ({
status: CaseStatuses.open,
severity: CaseSeverity.HIGH,
assignees: [],
+ customFields: [],
settings: {
syncAlerts: false,
},
diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/common.ts b/x-pack/test/security_solution_cypress/cypress/tasks/common.ts
index 7e45afcc42c49..3c2467c25f6ad 100644
--- a/x-pack/test/security_solution_cypress/cypress/tasks/common.ts
+++ b/x-pack/test/security_solution_cypress/cypress/tasks/common.ts
@@ -94,7 +94,7 @@ export const resetRulesTableState = () => {
export const cleanKibana = () => {
resetRulesTableState();
deleteAlertsAndRules();
- deleteCases();
+ deleteAllCasesItems();
deleteTimelines();
};
@@ -169,8 +169,8 @@ export const deleteAlertsIndex = () => {
});
};
-export const deleteCases = () => {
- const kibanaIndexUrl = `${Cypress.env('ELASTICSEARCH_URL')}/.kibana_\*`;
+export const deleteAllCasesItems = () => {
+ const kibanaIndexUrl = `${Cypress.env('ELASTICSEARCH_URL')}/.kibana_alerting_cases_\*`;
rootRequest({
method: 'POST',
url: `${kibanaIndexUrl}/_delete_by_query?conflicts=proceed&refresh`,
@@ -179,8 +179,34 @@ export const deleteCases = () => {
bool: {
filter: [
{
- match: {
- type: 'cases',
+ bool: {
+ should: [
+ {
+ term: {
+ type: 'cases',
+ },
+ },
+ {
+ term: {
+ type: 'cases-configure',
+ },
+ },
+ {
+ term: {
+ type: 'cases-comments',
+ },
+ },
+ {
+ term: {
+ type: 'cases-user-action',
+ },
+ },
+ {
+ term: {
+ type: 'cases-connector-mappings',
+ },
+ },
+ ],
},
},
],
@@ -191,7 +217,7 @@ export const deleteCases = () => {
};
export const deleteConnectors = () => {
- const kibanaIndexUrl = `${Cypress.env('ELASTICSEARCH_URL')}/.kibana_\*`;
+ const kibanaIndexUrl = `${Cypress.env('ELASTICSEARCH_URL')}/.kibana_alerting_cases_\*`;
rootRequest({
method: 'POST',
url: `${kibanaIndexUrl}/_delete_by_query?conflicts=proceed&refresh`,
diff --git a/x-pack/test_serverless/api_integration/services/svl_cases/api.ts b/x-pack/test_serverless/api_integration/services/svl_cases/api.ts
index 4163fc70291db..a504b71240dd5 100644
--- a/x-pack/test_serverless/api_integration/services/svl_cases/api.ts
+++ b/x-pack/test_serverless/api_integration/services/svl_cases/api.ts
@@ -133,6 +133,7 @@ export function SvlCasesApiServiceProvider({ getService }: FtrProviderContext) {
status: CaseStatuses.open,
updated_by: null,
category: null,
+ customFields: [],
};
},
diff --git a/yarn.lock b/yarn.lock
index 41c8328afe98d..1c36304e4eff5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4023,6 +4023,10 @@
version "0.0.0"
uid ""
+"@kbn/core-test-helpers-model-versions@link:packages/core/test-helpers/core-test-helpers-model-versions":
+ version "0.0.0"
+ uid ""
+
"@kbn/core-test-helpers-so-type-serializer@link:packages/core/test-helpers/core-test-helpers-so-type-serializer":
version "0.0.0"
uid ""
@@ -11385,7 +11389,7 @@ async@^1.4.2:
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=
-async@^3.1.0, async@^3.2.0, async@^3.2.3:
+async@^3.2.0, async@^3.2.3, async@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
@@ -20804,33 +20808,33 @@ launchdarkly-eventsource@1.4.4:
resolved "https://registry.yarnpkg.com/launchdarkly-eventsource/-/launchdarkly-eventsource-1.4.4.tgz#fa595af8602e487c61520787170376c6a1104459"
integrity sha512-GL+r2Y3WccJlhFyL2buNKel+9VaMnYpbE/FfCkOST5jSNSFodahlxtGyrE8o7R+Qhobyq0Ree4a7iafJDQi9VQ==
-launchdarkly-js-client-sdk@^2.22.1:
- version "2.22.1"
- resolved "https://registry.yarnpkg.com/launchdarkly-js-client-sdk/-/launchdarkly-js-client-sdk-2.22.1.tgz#e6064c79bc575eea0aa4364be41754d54d89ae6a"
- integrity sha512-EAdw7B8w4m/WZGmHHLj9gbYBP6lCqJs5TQDCM9kWJOnvHBz7DJIxOdqazNMDn5AzBxfvaMG7cpLms+Cur5LD5g==
+launchdarkly-js-client-sdk@^3.1.4:
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/launchdarkly-js-client-sdk/-/launchdarkly-js-client-sdk-3.1.4.tgz#e613cb53412533c07ccf140ae570fc994c59758d"
+ integrity sha512-yq0FeklpVuHMSRz7jfUAfyM7I/659RvGztqJ0Y9G5eN/ZrG1o2W61ZU0Nrv/gqZCtLXjarh/u1otxSFFBjTpHw==
dependencies:
- escape-string-regexp "^1.0.5"
- launchdarkly-js-sdk-common "3.6.0"
+ escape-string-regexp "^4.0.0"
+ launchdarkly-js-sdk-common "5.0.3"
-launchdarkly-js-sdk-common@3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/launchdarkly-js-sdk-common/-/launchdarkly-js-sdk-common-3.6.0.tgz#d146be5bbd86a019c4bedc52e66c37a1ffa7bb3d"
- integrity sha512-wCdBoBiYXlP64jTrC0dOXY2B345LSJO/IvitbdW4kBKmJ1DkeufpqV0s5DBlwE0RLzDmaQx3mRTmcoNAIhIoaA==
+launchdarkly-js-sdk-common@5.0.3:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/launchdarkly-js-sdk-common/-/launchdarkly-js-sdk-common-5.0.3.tgz#345f899f5779be8b03d6599978c855eb838d8b7f"
+ integrity sha512-wKG8UsVbPVq8+7eavgAm5CVmulQWN6Ddod2ZoA3euZ1zPvJPwIQ2GrOYaCJr3cFrrMIX+nQyBJHBHYxUAPcM+Q==
dependencies:
base64-js "^1.3.0"
fast-deep-equal "^2.0.1"
- uuid "^3.3.2"
+ uuid "^8.0.0"
-launchdarkly-node-server-sdk@^6.4.2:
- version "6.4.2"
- resolved "https://registry.yarnpkg.com/launchdarkly-node-server-sdk/-/launchdarkly-node-server-sdk-6.4.2.tgz#10a4fea21762315a095a9377cb23dc8d6e714469"
- integrity sha512-cZQ/FDpzrXu7rOl2re9+79tX/jOrj+kb1ikbqpk/jEgLvXUHGE7Xr+fsEIbQa80H1PkGwiyWbmnAl31THJfKew==
+launchdarkly-node-server-sdk@^7.0.3:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/launchdarkly-node-server-sdk/-/launchdarkly-node-server-sdk-7.0.3.tgz#d7a8b996d992b0ca5d4972db5df1ae49332b094c"
+ integrity sha512-uSkBezAiQ9nwv8N6CmI7OmyJ9e3xpueJzYOso8+5vMf7VtBtPjz6RRsUkUsSzUDo7siclmW8USjCwqn9aX2EbQ==
dependencies:
- async "^3.1.0"
+ async "^3.2.4"
launchdarkly-eventsource "1.4.4"
lru-cache "^6.0.0"
node-cache "^5.1.0"
- semver "^7.3.0"
+ semver "^7.5.4"
tunnel "0.0.6"
uuid "^8.3.2"
@@ -27077,7 +27081,7 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semve
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
-semver@^7.3.0, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.0, semver@^7.5.2, semver@^7.5.3:
+semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.0, semver@^7.5.2, semver@^7.5.3:
version "7.5.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e"
integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==
@@ -29964,7 +29968,7 @@ uuid@^3.3.2, uuid@^3.3.3:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
-uuid@^8.3.0, uuid@^8.3.2:
+uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==