diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 24c1fc13a5753..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 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-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/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/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/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap index 0f4e6becae335..dac7b7a8d034b 100644 --- a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap @@ -1435,6 +1435,16 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the } } }, + "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": { @@ -2817,6 +2827,20 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the } } }, + "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/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts index 5293a026e5b48..0cbd35a8f3bd6 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts @@ -164,6 +164,66 @@ describe('data telemetry collection tasks', () => { }); }); + 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/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.test.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.test.ts index 8f4b0d63c9947..c7f6655141b2d 100644 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.test.ts +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.test.ts @@ -40,6 +40,9 @@ describe('LaunchDarklyClient - browser', () => { avatar: 'fake-blue-avatar', ip: 'my-weird-ip', country: 'distributed', + // intentionally adding this to make sure the code is overriding appropriately + kind: 'other kind', + key: 'other user', }; const extraFields = { @@ -52,9 +55,10 @@ describe('LaunchDarklyClient - browser', () => { expect(launchDarklyLibraryMock.initialize).toHaveBeenCalledWith( 'fake-client-id', { - key: 'fake-user-id', ...topFields, - custom: extraFields, + ...extraFields, + kind: 'user', + key: 'fake-user-id', }, { application: { id: 'kibana-browser', version: 'version' }, @@ -73,8 +77,9 @@ describe('LaunchDarklyClient - browser', () => { expect(launchDarklyLibraryMock.initialize).toHaveBeenCalledWith( 'fake-client-id', { + kind: 'user', key: 'fake-user-id', - custom: { kibanaVersion: 'version' }, + kibanaVersion: 'version', }, { application: { id: 'kibana-browser', version: 'version' }, @@ -92,8 +97,9 @@ describe('LaunchDarklyClient - browser', () => { expect(launchDarklyLibraryMock.initialize).toHaveBeenCalledWith( 'fake-client-id', { + kind: 'user', key: 'fake-user-id', - custom: { kibanaVersion: 'version' }, + kibanaVersion: 'version', }, { application: { id: 'kibana-browser', version: 'version' }, @@ -107,8 +113,9 @@ describe('LaunchDarklyClient - browser', () => { // Update user metadata a 2nd time await client.updateUserMetadata({ userId: 'fake-user-id', kibanaVersion: 'version' }); expect(ldClientMock.identify).toHaveBeenCalledWith({ + kind: 'user', key: 'fake-user-id', - custom: { kibanaVersion: 'version' }, + kibanaVersion: 'version', }); }); }); diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.ts index f78286f0fa8ca..34d639bd1dc33 100644 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.ts +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/public/launch_darkly_client/launch_darkly_client.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { type LDClient, type LDUser, type LDLogLevel } from 'launchdarkly-js-client-sdk'; +import { + type LDClient, + type LDSingleKindContext, + type LDLogLevel, +} from 'launchdarkly-js-client-sdk'; export interface LaunchDarklyClientConfig { client_id: string; @@ -15,14 +19,6 @@ export interface LaunchDarklyClientConfig { export interface LaunchDarklyUserMetadata extends Record { userId: string; - // We are not collecting any of the above, but this is to match the LDUser first-level definition - name?: string; - firstName?: string; - lastName?: string; - email?: string; - avatar?: string; - ip?: string; - country?: string; } export class LaunchDarklyClient { @@ -34,19 +30,11 @@ export class LaunchDarklyClient { ) {} public async updateUserMetadata(userMetadata: LaunchDarklyUserMetadata) { - const { userId, name, firstName, lastName, email, avatar, ip, country, ...custom } = - userMetadata; - const launchDarklyUser: LDUser = { + const { userId, ...userMetadataWithoutUserId } = userMetadata; + const launchDarklyUser: LDSingleKindContext = { + ...userMetadataWithoutUserId, + kind: 'user', key: userId, - name, - firstName, - lastName, - email, - avatar, - ip, - country, - // This casting is needed because LDUser does not allow `Record` - custom: custom as Record, }; if (this.launchDarklyClient) { await this.launchDarklyClient.identify(launchDarklyUser); diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.test.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.test.ts index cc140ea44ffdb..7d40fe6d10ccf 100644 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.test.ts +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.test.ts @@ -80,7 +80,7 @@ describe('LaunchDarklyClient - server', () => { }); describe('updateUserMetadata', () => { - test('sets the top-level properties at the root (renaming userId to key) and the rest under `custom`', () => { + test('sets all properties at the root level, renaming userId to key (no nesting into custom)', () => { expect(client).toHaveProperty('launchDarklyUser', undefined); const topFields = { @@ -91,6 +91,9 @@ describe('LaunchDarklyClient - server', () => { avatar: 'fake-blue-avatar', ip: 'my-weird-ip', country: 'distributed', + // intentionally adding this to make sure the code is overriding appropriately + kind: 'other kind', + key: 'other user', }; const extraFields = { @@ -101,9 +104,10 @@ describe('LaunchDarklyClient - server', () => { client.updateUserMetadata({ userId: 'fake-user-id', ...topFields, ...extraFields }); expect(client).toHaveProperty('launchDarklyUser', { - key: 'fake-user-id', ...topFields, - custom: extraFields, + ...extraFields, + kind: 'user', + key: 'fake-user-id', }); }); @@ -113,8 +117,9 @@ describe('LaunchDarklyClient - server', () => { client.updateUserMetadata({ userId: 'fake-user-id', kibanaVersion: 'version' }); expect(client).toHaveProperty('launchDarklyUser', { + kind: 'user', key: 'fake-user-id', - custom: { kibanaVersion: 'version' }, + kibanaVersion: 'version', }); }); }); @@ -132,7 +137,7 @@ describe('LaunchDarklyClient - server', () => { expect(ldClientMock.variation).toHaveBeenCalledTimes(1); expect(ldClientMock.variation).toHaveBeenCalledWith( 'my-feature-flag', - { key: 'fake-user-id', custom: { kibanaVersion: 'version' } }, + { kind: 'user', key: 'fake-user-id', kibanaVersion: 'version' }, 123 ); }); @@ -150,7 +155,7 @@ describe('LaunchDarklyClient - server', () => { expect(ldClientMock.track).toHaveBeenCalledTimes(1); expect(ldClientMock.track).toHaveBeenCalledWith( 'my-feature-flag', - { key: 'fake-user-id', custom: { kibanaVersion: 'version' } }, + { kind: 'user', key: 'fake-user-id', kibanaVersion: 'version' }, {}, 123 ); @@ -183,8 +188,9 @@ describe('LaunchDarklyClient - server', () => { }); expect(ldClientMock.allFlagsState).toHaveBeenCalledTimes(1); expect(ldClientMock.allFlagsState).toHaveBeenCalledWith({ + kind: 'user', key: 'fake-user-id', - custom: { kibanaVersion: 'version' }, + kibanaVersion: 'version', }); }); }); diff --git a/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.ts b/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.ts index 10126e6d48a46..53554ff242e07 100644 --- a/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.ts +++ b/x-pack/plugins/cloud_integrations/cloud_experiments/server/launch_darkly_client/launch_darkly_client.ts @@ -9,7 +9,7 @@ import LaunchDarkly, { type LDClient, type LDFlagSet, type LDLogLevel, - type LDUser, + type LDSingleKindContext, } from 'launchdarkly-node-server-sdk'; import type { Logger } from '@kbn/core/server'; @@ -41,7 +41,7 @@ export interface LaunchDarklyGetAllFlags { export class LaunchDarklyClient { private readonly launchDarklyClient: LDClient; - private launchDarklyUser?: LDUser; + private launchDarklyUser?: LDSingleKindContext; constructor(ldConfig: LaunchDarklyClientConfig, private readonly logger: Logger) { this.launchDarklyClient = LaunchDarkly.init(ldConfig.sdk_key, { @@ -59,19 +59,11 @@ export class LaunchDarklyClient { } public updateUserMetadata(userMetadata: LaunchDarklyUserMetadata) { - const { userId, name, firstName, lastName, email, avatar, ip, country, ...custom } = - userMetadata; + const { userId, ...userMetadataWithoutUserId } = userMetadata; this.launchDarklyUser = { + ...userMetadataWithoutUserId, + kind: 'user', key: userId, - name, - firstName, - lastName, - email, - avatar, - ip, - country, - // This casting is needed because LDUser does not allow `Record` - custom: custom as Record, }; } diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index e403222815b9a..843f277c11eb5 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -83,13 +83,13 @@ const App: FC = ({ coreStart, deps, appMountParams, isServerless, mlFe kibanaVersion: deps.kibanaVersion, share: deps.share, data: deps.data, + dataViewEditor: deps.dataViewEditor, security: deps.security, licenseManagement: deps.licenseManagement, storage: localStorage, embeddable: deps.embeddable, maps: deps.maps, triggersActionsUi: deps.triggersActionsUi, - dataViewEditor: deps.dataViewEditor, dataVisualizer: deps.dataVisualizer, usageCollection: deps.usageCollection, fieldFormats: deps.fieldFormats, diff --git a/x-pack/plugins/ml/public/application/components/create_data_view_button/create_data_view_button.tsx b/x-pack/plugins/ml/public/application/components/create_data_view_button/create_data_view_button.tsx new file mode 100644 index 0000000000000..069b5589eb058 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/create_data_view_button/create_data_view_button.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useCallback, useEffect, useRef } from 'react'; +import { useMlKibana } from '../../contexts/kibana'; + +export const CreateDataViewButton = ({ + onDataViewCreated, + allowAdHocDataView = false, +}: { + onDataViewCreated: (id: string, type: string, name?: string) => void; + allowAdHocDataView?: boolean; +}) => { + const { dataViewEditor } = useMlKibana().services; + const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView()); + const closeDataViewEditorRef = useRef<() => void | undefined>(); + + const createNewDataView = useCallback(() => { + closeDataViewEditorRef.current = dataViewEditor?.openEditor({ + onSave: async (dataView) => { + if (dataView.id && onDataViewCreated) { + onDataViewCreated(dataView.id, 'index-pattern', dataView.name); + } + }, + + allowAdHocDataView, + }); + }, [onDataViewCreated, dataViewEditor, allowAdHocDataView]); + + useEffect(function cleanUpFlyout() { + return () => { + // Close the editor when unmounting + if (closeDataViewEditorRef.current) { + closeDataViewEditorRef.current(); + } + }; + }, []); + + return canEditDataView ? ( + + + + ) : null; +}; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/read_tags/read_tags_route.ts b/x-pack/plugins/ml/public/application/components/create_data_view_button/index.ts similarity index 64% rename from x-pack/plugins/security_solution/common/api/detection_engine/rule_management/read_tags/read_tags_route.ts rename to x-pack/plugins/ml/public/application/components/create_data_view_button/index.ts index 4b651600e9c9e..ef0244c8a1c17 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/read_tags/read_tags_route.ts +++ b/x-pack/plugins/ml/public/application/components/create_data_view_button/index.ts @@ -5,7 +5,4 @@ * 2.0. */ -import * as t from 'io-ts'; - -export const ReadTagsResponse = t.array(t.string); -export type ReadTagsResponse = t.TypeOf; +export { CreateDataViewButton } from './create_data_view_button'; diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx index f9dc782e1e23a..592df6079603d 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx @@ -159,7 +159,7 @@ export const AddInferencePipelineFlyout: FC = ( {step === ADD_INFERENCE_PIPELINE_STEPS.TEST && ( )} - {step === ADD_INFERENCE_PIPELINE_STEPS.CREATE && sourceIndex && ( + {step === ADD_INFERENCE_PIPELINE_STEPS.CREATE && ( = ({ @@ -202,7 +202,7 @@ export const ReviewAndCreatePipeline: FC = ({ - {pipelineCreated ? ( + {pipelineCreated && sourceIndex ? ( <> { const onSearchSelected = async ( id: string, type: string, - fullName: string, - savedObject: SavedObjectCommon + fullName?: string, + savedObject?: SavedObjectCommon ) => { // Kibana data views including `:` are cross-cluster search indices // and are not supported by Data Frame Analytics yet. For saved searches @@ -47,7 +48,7 @@ export const SourceSelection: FC = () => { // the selection before redirecting and show an error callout instead. let dataViewName = ''; - if (type === 'index-pattern') { + if (type === 'index-pattern' && savedObject) { dataViewName = getNestedProperty(savedObject, 'attributes.title'); } else if (type === 'search') { try { @@ -71,7 +72,7 @@ export const SourceSelection: FC = () => { } } - if (isCcsIndexPattern(dataViewName)) { + if (isCcsIndexPattern(dataViewName) && savedObject) { setIsCcsCallOut(true); if (type === 'search') { setCcsCallOutBodyText( @@ -157,7 +158,9 @@ export const SourceSelection: FC = () => { contentClient: contentManagement.client, uiSettings, }} - /> + > + + diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx index c4b81907e52b9..0bfa0ad5acdb7 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useCallback } from 'react'; import { EuiPageBody, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public'; +import { CreateDataViewButton } from '../../../../components/create_data_view_button'; import { useMlKibana, useNavigateToPath } from '../../../../contexts/kibana'; import { MlPageHeader } from '../../../../components/page_header'; @@ -23,13 +24,16 @@ export const Page: FC = ({ nextStepPath }) => { const { contentManagement, uiSettings } = useMlKibana().services; const navigateToPath = useNavigateToPath(); - const onObjectSelection = (id: string, type: string) => { - navigateToPath( - `${nextStepPath}?${type === 'index-pattern' ? 'index' : 'savedSearchId'}=${encodeURIComponent( - id - )}` - ); - }; + const onObjectSelection = useCallback( + (id: string, type: string, name?: string) => { + navigateToPath( + `${nextStepPath}?${ + type === 'index-pattern' ? 'index' : 'savedSearchId' + }=${encodeURIComponent(id)}` + ); + }, + [navigateToPath, nextStepPath] + ); return (
@@ -75,7 +79,9 @@ export const Page: FC = ({ nextStepPath }) => { contentClient: contentManagement.client, uiSettings, }} - /> + > + +
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/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_management_ui/components/ml_rule_warning_popover/ml_rule_warning_popover.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/ml_rule_warning_popover/ml_rule_warning_popover.tsx index 493686f9ceb19..7a18a976ab650 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/ml_rule_warning_popover/ml_rule_warning_popover.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/ml_rule_warning_popover/ml_rule_warning_popover.tsx @@ -15,7 +15,7 @@ import { EuiButtonIcon, } from '@elastic/eui'; -import { RuleExecutionStatus } from '../../../../../common/api/detection_engine/rule_monitoring'; +import { RuleExecutionStatusEnum } from '../../../../../common/api/detection_engine/rule_monitoring'; import type { SecurityJob } from '../../../../common/components/ml_popover/types'; import * as i18n from '../rules_table/translations'; @@ -64,7 +64,7 @@ const MlRuleWarningPopoverComponent: React.FC ); - 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/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/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/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/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/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/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/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==