diff --git a/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml b/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml index dd6fd12c7c013..aabbcbd3255a4 100644 --- a/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml +++ b/.buildkite/pipelines/es_serverless/verify_es_serverless_image.yml @@ -50,7 +50,7 @@ steps: agents: queue: n2-4-spot depends_on: build - timeout_in_minutes: 40 + timeout_in_minutes: 60 parallelism: 2 retry: automatic: @@ -74,8 +74,8 @@ steps: agents: queue: n2-4-spot depends_on: build - timeout_in_minutes: 120 - parallelism: 2 + timeout_in_minutes: 60 + parallelism: 4 retry: automatic: - exit_status: '*' diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index ed33671ee2ed1..d567ac16af588 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -84,7 +84,7 @@ steps: agents: queue: n2-4-spot depends_on: build - timeout_in_minutes: 40 + timeout_in_minutes: 60 parallelism: 2 retry: automatic: @@ -108,8 +108,8 @@ steps: agents: queue: n2-4-spot depends_on: build - timeout_in_minutes: 120 - parallelism: 2 + timeout_in_minutes: 60 + parallelism: 4 retry: automatic: - exit_status: '*' diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 977e46e2beaea..04861de3dfac9 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -62,49 +62,49 @@ steps: agents: queue: n2-4-spot depends_on: build - timeout_in_minutes: 40 + timeout_in_minutes: 60 parallelism: 2 retry: automatic: - exit_status: '*' limit: 1 - # status_exception: Native role management is not enabled in this Elasticsearch instance - # - command: .buildkite/scripts/steps/functional/security_serverless_defend_workflows.sh - # label: 'Serverless Security Defend Workflows Cypress Tests' - # agents: - # queue: n2-4-spot - # depends_on: build - # timeout_in_minutes: 40 - # retry: - # automatic: - # - exit_status: '*' - # limit: 1 - - - command: .buildkite/scripts/steps/functional/security_serverless_investigations.sh - label: 'Serverless Security Investigations Cypress Tests' + - command: .buildkite/scripts/steps/functional/security_serverless_explore.sh + label: 'Serverless Explore - Security Solution Cypress Tests' agents: queue: n2-4-spot depends_on: build - timeout_in_minutes: 40 + timeout_in_minutes: 60 parallelism: 2 retry: automatic: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/functional/security_serverless_explore.sh - label: 'Serverless Security Explore Cypress Tests' + - command: .buildkite/scripts/steps/functional/security_serverless_investigations.sh + label: 'Serverless Investigations - Security Solution Cypress Tests' agents: queue: n2-4-spot depends_on: build - timeout_in_minutes: 40 - parallelism: 2 + timeout_in_minutes: 60 + parallelism: 4 retry: automatic: - exit_status: '*' limit: 1 + # status_exception: Native role management is not enabled in this Elasticsearch instance + # - command: .buildkite/scripts/steps/functional/security_serverless_defend_workflows.sh + # label: 'Serverless Security Defend Workflows Cypress Tests' + # agents: + # queue: n2-4-spot + # depends_on: build + # timeout_in_minutes: 60 + # retry: + # automatic: + # - exit_status: '*' + # limit: 1 + - command: .buildkite/scripts/steps/lint.sh label: 'Linting' agents: diff --git a/package.json b/package.json index 47698666d409f..aea628679cb99 100644 --- a/package.json +++ b/package.json @@ -1563,7 +1563,7 @@ "pixelmatch": "^5.3.0", "playwright": "=1.37.0", "pngjs": "^3.4.0", - "postcss": "^8.4.14", + "postcss": "^8.4.31", "postcss-loader": "^4.2.0", "postcss-prefix-selector": "^1.16.0", "postcss-scss": "^4.0.4", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts index b5a1591e7a5bd..26ab99d41633d 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts @@ -107,7 +107,7 @@ describe('checking migration metadata changes on all registered SO types', () => "ingest-agent-policies": "f11cc19275f4c3e4ee7c5cd6423b6706b21b989d", "ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d", "ingest-outputs": "b4e636b13a5d0f89f0400fb67811d4cca4736eb0", - "ingest-package-policies": "8ec637429836f80f1fcc798bcee7c5916eceaed5", + "ingest-package-policies": "a0c9fb48e04dcd638e593db55f1c6451523f90ea", "ingest_manager_settings": "64955ef1b7a9ffa894d4bb9cf863b5602bfa6885", "inventory-view": "b8683c8e352a286b4aca1ab21003115a4800af83", "kql-telemetry": "93c1d16c1a0dfca9c8842062cf5ef8f62ae401ad", diff --git a/x-pack/plugins/cloud_security_posture/README.md b/x-pack/plugins/cloud_security_posture/README.md index f9c760fbeb99f..0befebb667de6 100755 --- a/x-pack/plugins/cloud_security_posture/README.md +++ b/x-pack/plugins/cloud_security_posture/README.md @@ -6,17 +6,21 @@ Cloud Posture automates the identification and remediation of risks across cloud ## Development -read [Kibana Contributing Guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for more details +Read [Kibana Contributing Guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for more details ## Testing -for general guidelines, read [Kibana Testing Guide](https://www.elastic.co/guide/en/kibana/current/development-tests.html) for more details +For general guidelines, read [Kibana Testing Guide](https://www.elastic.co/guide/en/kibana/current/development-tests.html) for more details ### Tests 1. Unit Tests (Jest) - located in sibling files to the source code -2. [Integration Tests](../../test/api_integration/apis/cloud_security_posture/index.ts) -3. [End-to-End Tests](../../test/cloud_security_posture_functional/pages/index.ts) +1. [API Integration Tests](../../test/api_integration/apis/cloud_security_posture/config.ts) +1. [Telemetry Integration Tests](../../test/cloud_security_posture_api/config.ts) +1. [End-to-End Tests](../../test/cloud_security_posture_functional/config.ts) +1. [Serverless API Integration tests](../../test_serverless/api_integration/test_suites/security/config.ts) +1. [Serverless End-to-End Tests](../../test_serverless/functional/test_suites/security/config.ts) + ### Tools @@ -32,6 +36,17 @@ Run **ESLint**: yarn lint:es x-pack/plugins/cloud_security_posture ``` +Run **i18n check**: +```bash +node scripts/i18n_check.js +``` + +> **Note** +> +> i18n should run on project scope as it checks translations files outside of our plugin. +> +> Fixes can be applied using the --fix flag + Run [**Unit Tests**](https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing): ```bash @@ -39,6 +54,7 @@ yarn test:jest --config x-pack/plugins/cloud_security_posture/jest.config.js ``` > **Note** +> > for a coverage report, add the `--coverage` flag, and run `open target/kibana-coverage/jest/x-pack/plugins/cloud_security_posture/index.html` Run [**Integration Tests**](https://docs.elastic.dev/kibana-dev-docs/tutorials/testing-plugins#): @@ -50,14 +66,45 @@ yarn test:ftr --config x-pack/test/api_integration/config.ts Run [**End-to-End Tests**](https://www.elastic.co/guide/en/kibana/current/development-tests.html#_running_functional_tests): ```bash -yarn test:ftr --config x-pack/test/cloud_security_posture_functional/config.ts --debug +yarn test:ftr --config x-pack/test/cloud_security_posture_functional/config.ts +yarn test:ftr --config x-pack/test/api_integration/config.ts --include-tag=cloud_security_posture +yarn test:ftr --config x-pack/test/cloud_security_posture_api/config.ts +yarn test:ftr --config x-pack/test_serverless/api_integration/test_suites/security/config.ts --include-tag=cloud_security_posture +yarn test:ftr --config x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.ts ``` -
+#### Run **FTR tests (integration or e2e) for development** + +Functional test runner (FTR) can be used separately with `ftr:runner` and `ftr:server`. This is convenient while developing tests. -test runner (FTR) can be used separately with `ftr:runner` and `ftr:server`: +For example, +run ESS (stateful) api integration tests: ```bash yarn test:ftr:server --config x-pack/test/api_integration/config.ts -yarn test:ftr:runner --include-tag=cloud_security_posture --config x-pack/test/api_integration/config.ts +yarn test:ftr:runner --config x-pack/test/api_integration/apis/cloud_security_posture/config.ts +``` + +run ESS (stateful) telemetry integration tests: +```bash +yarn test:ftr:server --config x-pack/test/cloud_security_posture_api/config.ts +yarn test:ftr:runner --config x-pack/test/cloud_security_posture_api/config.ts ``` + +run ESS (stateful) e2e tests: +```bash +yarn test:ftr:server --config x-pack/test/cloud_security_posture_functional/config.ts +yarn test:ftr:runner --config x-pack/test/cloud_security_posture_functional/config.ts +``` + +run serverless api integration tests: +```bash +yarn test:ftr:server --config x-pack/test_serverless/api_integration/test_suites/security/config.ts +yarn test:ftr:runner --config x-pack/test_serverless/api_integration/test_suites/security/config.ts --include-tag=cloud_security_posture +``` + +run serverless e2e tests: +```bash +yarn test:ftr:server --config x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.ts +yarn test:ftr:runner ---config x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.ts +``` \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 453a6346f8690..6771168239126 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -18,12 +18,20 @@ import { shallow } from 'enzyme'; import { VersionMismatchPage } from '../shared/version_mismatch'; import { WorkplaceSearchHeaderActions } from './components/layout'; +import { SourcesRouter } from './views/content_sources'; import { SourceAdded } from './views/content_sources/components/source_added'; import { ErrorState } from './views/error_state'; +import { NotFound } from './views/not_found'; import { Overview } from './views/overview'; +import { RoleMappings } from './views/role_mappings'; import { SetupGuide } from './views/setup_guide'; -import { WorkplaceSearch, WorkplaceSearchUnconfigured, WorkplaceSearchConfigured } from '.'; +import { + WorkplaceSearch, + WorkplaceSearchUnconfigured, + WorkplaceSearchConfigured, + WorkplaceSearchConfiguredRoutes, +} from '.'; describe('WorkplaceSearch', () => { it('renders VersionMismatchPage when there are mismatching versions', () => { @@ -89,24 +97,33 @@ describe('WorkplaceSearchConfigured', () => { }); it('renders chrome and header actions', () => { - setMockValues({ organization: { kibanaUIsEnabled: false } }); - const wrapper = shallow(); - expect(wrapper.find(Overview)).toHaveLength(1); + setMockValues({ + account: { isAdmin: true }, + organization: { kibanaUIsEnabled: true }, + }); + const props = { isAdmin: true, kibanaUIsEnabled: true }; + const wrapperConfiguredRoutes = shallow(); + expect(wrapperConfiguredRoutes.find(Overview)).toHaveLength(1); + shallow(); expect(mockKibanaValues.setChromeIsVisible).toHaveBeenCalledWith(true); expect(mockKibanaValues.renderHeaderActions).toHaveBeenCalledWith(WorkplaceSearchHeaderActions); }); it('initializes app data with passed props', () => { const { workplaceSearch } = DEFAULT_INITIAL_APP_DATA; - setMockValues({ organization: { kibanaUIsEnabled: false } }); + setMockValues({ account: { isAdmin: true }, organization: { kibanaUIsEnabled: false } }); shallow(); expect(initializeAppData).toHaveBeenCalledWith({ workplaceSearch }); }); it('does not re-initialize app data or re-render header actions', () => { - setMockValues({ hasInitialized: true, organization: { kibanaUIsEnabled: false } }); + setMockValues({ + account: { isAdmin: true }, + hasInitialized: true, + organization: { kibanaUIsEnabled: false }, + }); shallow(); @@ -115,14 +132,57 @@ describe('WorkplaceSearchConfigured', () => { }); it('renders SourceAdded', () => { - setMockValues({ organization: { kibanaUIsEnabled: true } }); - const wrapper = shallow(); + setMockValues({ organization: { kibanaUIsEnabled: true }, account: { isAdmin: true } }); + const props = { isAdmin: true, kibanaUIsEnabled: true }; + const wrapper = shallow(); expect(wrapper.find(SourceAdded)).toHaveLength(1); }); - it('renders Overview when kibanaUIsEnabled is true', () => { - setMockValues({ organization: { kibanaUIsEnabled: false } }); - const wrapper = shallow(); - expect(wrapper.find(Overview)).toHaveLength(1); + describe('when admin user is logged in', () => { + it('all routes accessible when kibanaUIsEnabled is true', () => { + setMockValues({ + account: { isAdmin: true }, + organization: { kibanaUIsEnabled: true }, + }); + const props = { isAdmin: true, kibanaUIsEnabled: true }; + + const wrapper = shallow(); + expect(wrapper.find(RoleMappings)).toHaveLength(1); + }); + + it('only Overview and Notfound routes are available when kibanaUIsEnabled is false', () => { + setMockValues({ + account: { isAdmin: true }, + organization: { kibanaUIsEnabled: false }, + }); + const props = { isAdmin: true, kibanaUIsEnabled: false }; + + const wrapper = shallow(); + expect(wrapper.find(RoleMappings)).toHaveLength(0); + expect(wrapper.find(Overview)).toHaveLength(1); + expect(wrapper.find(NotFound)).toHaveLength(1); + }); + }); + describe('when non admin user is logged in, all routes are accessible', () => { + it('when kibanaUIsEnabled is true ', () => { + setMockValues({ + account: { isAdmin: false }, + organization: { kibanaUIsEnabled: true }, + }); + const props = { isAdmin: true, kibanaUIsEnabled: true }; + + const wrapper = shallow(); + expect(wrapper.find(RoleMappings)).toHaveLength(1); + }); + it('when kibanaUIsEnabled is false ', () => { + setMockValues({ + account: { isAdmin: false }, + organization: { kibanaUIsEnabled: false }, + }); + const props = { isAdmin: false, kibanaUIsEnabled: false }; + + const wrapper = shallow(); + expect(wrapper.find(SourcesRouter)).toHaveLength(2); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 74921301669e7..e9332fe81bdab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -73,10 +73,80 @@ export const WorkplaceSearch: React.FC = (props) => { return ; }; +export const WorkplaceSearchConfiguredRoutes: React.FC<{ + isAdmin: boolean; + kibanaUIsEnabled: boolean; +}> = ({ isAdmin, kibanaUIsEnabled }) => { + const isblockingRoutes = isAdmin && !kibanaUIsEnabled; + return !isblockingRoutes ? ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + + + + + + + + + ); +}; export const WorkplaceSearchConfigured: React.FC = (props) => { const { hasInitialized, organization: { kibanaUIsEnabled }, + account: { isAdmin }, } = useValues(AppLogic); const { initializeAppData, setContext } = useActions(AppLogic); const { renderHeaderActions, setChromeIsVisible } = useValues(KibanaLogic); @@ -100,65 +170,7 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { } }, [hasInitialized]); - return ( - - - - - {kibanaUIsEnabled && ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )} - - - - - - ); + return ; }; export const WorkplaceSearchUnconfigured: React.FC = () => ( diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index b21af06c38349..9bf37677ae905 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -30,17 +30,14 @@ import { migratePackagePolicyToV8110, } from './migrations/security_solution/to_v8_11_0'; +import { migrateCspPackagePolicyToV8110 } from './migrations/cloud_security_posture'; + import { migrateOutputEvictionsFromV8100, migrateOutputToV8100 } from './migrations/to_v8_10_0'; import { migrateSyntheticsPackagePolicyToV8100 } from './migrations/synthetics/to_v8_10_0'; import { migratePackagePolicyEvictionsFromV8100 } from './migrations/security_solution/to_v8_10_0'; -import { - migratePackagePolicyEvictionsFromV81102, - migratePackagePolicyToV81102, -} from './migrations/security_solution/to_v8_11_0_2'; - import { migrateAgentPolicyToV7100, migratePackagePolicyToV7100, @@ -78,6 +75,10 @@ import { } from './migrations/security_solution'; import { migratePackagePolicyToV880 } from './migrations/to_v8_8_0'; import { migrateAgentPolicyToV890 } from './migrations/to_v8_9_0'; +import { + migratePackagePolicyToV81102, + migratePackagePolicyEvictionsFromV81102, +} from './migrations/security_solution/to_v8_11_0_2'; /* * Saved object types and mappings @@ -351,6 +352,14 @@ const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({ forwardCompatibility: migratePackagePolicyEvictionsFromV81102, }, }, + '4': { + changes: [ + { + type: 'data_backfill', + backfillFn: migrateCspPackagePolicyToV8110, + }, + ], + }, }, migrations: { '7.10.0': migratePackagePolicyToV7100, diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/cloud_security_posture/index.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/cloud_security_posture/index.ts new file mode 100644 index 0000000000000..d9f2adb59e575 --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/cloud_security_posture/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { migrateCspPackagePolicyToV8110 } from './to_v8_11_0'; diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/cloud_security_posture/to_v8_11_0.test.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/cloud_security_posture/to_v8_11_0.test.ts new file mode 100644 index 0000000000000..ec6313080642b --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/cloud_security_posture/to_v8_11_0.test.ts @@ -0,0 +1,125 @@ +/* + * 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 { SavedObjectModelTransformationContext } from '@kbn/core-saved-objects-server'; + +import { migrateCspPackagePolicyToV8110 as migration } from './to_v8_11_0'; + +describe('8.11.0 Cloud Security Posture Package Policy migration', () => { + const policyDoc = ( + accountType: string, + isAccountTypeCorrect: boolean, + packageName: string + ): any => { + return { + id: 'mock-saved-csp-object-id', + attributes: { + name: 'cloud_security_posture_test', + package: { + name: packageName, + title: '', + version: '', + }, + id: 'ID_123', + policy_id: '', + enabled: true, + namespace: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: accountType, + enabled: true, + streams: [ + { + vars: { + ...(isAccountTypeCorrect && { + 'gcp.account_type': { value: 'single-account', type: 'text' }, + }), + }, + }, + ], + config: {}, + }, + ], + }, + type: ' nested', + }; + }; + + it('adds gcp.account_type to policy, set to single', () => { + const initialDoc = policyDoc('cloudbeat/cis_gcp', false, 'cloud_security_posture'); + const migratedDoc = policyDoc('cloudbeat/cis_gcp', true, 'cloud_security_posture'); + expect(migration(initialDoc, {} as SavedObjectModelTransformationContext)).toEqual({ + attributes: migratedDoc.attributes, + }); + }); + + it('if there are no type cloudbeat/cis_gcp, do not add gcp.account_type', () => { + const initialDoc = policyDoc('cloudbeat/cis_aws', false, 'cloud_security_posture'); + const migratedDoc = policyDoc('cloudbeat/cis_aws', false, 'cloud_security_posture'); + expect(migration(initialDoc, {} as SavedObjectModelTransformationContext)).toEqual({ + attributes: migratedDoc.attributes, + }); + }); + + it('if there are no cloud_security_posture package, do not change the doc', () => { + const initialDoc = policyDoc('cloudbeat/cis_gcp', false, 'NOT_cloud_security_posture'); + const migratedDoc = policyDoc('cloudbeat/cis_gcp', false, 'NOT_cloud_security_posture'); + expect(migration(initialDoc, {} as SavedObjectModelTransformationContext)).toEqual({ + attributes: migratedDoc.attributes, + }); + }); + + it('if gcp.account_type exist and already has a value, do not set it to single-account', () => { + const policyDocWithAccountType = (): any => { + return { + id: 'mock-saved-csp-object-id', + attributes: { + name: 'cloud_security_posture_test', + package: { + name: 'cloud_security_posture', + title: '', + version: '', + }, + id: 'ID_1234', + policy_id: '', + enabled: true, + namespace: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'cloudbeat/cis_gcp', + enabled: true, + streams: [ + { + vars: { + 'gcp.account_type': { value: 'single-account-MAYBE', type: 'text' }, + }, + }, + ], + config: {}, + }, + ], + }, + type: ' nested', + }; + }; + const initialDoc = policyDocWithAccountType(); + const migratedDoc = policyDocWithAccountType(); + expect(migration(initialDoc, {} as SavedObjectModelTransformationContext)).toEqual({ + attributes: migratedDoc.attributes, + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/cloud_security_posture/to_v8_11_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/cloud_security_posture/to_v8_11_0.ts new file mode 100644 index 0000000000000..dd6760a9dc4ac --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/cloud_security_posture/to_v8_11_0.ts @@ -0,0 +1,36 @@ +/* + * 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 { SavedObjectModelDataBackfillFn } from '@kbn/core-saved-objects-server'; + +import type { PackagePolicy } from '../../../../common'; + +export const migrateCspPackagePolicyToV8110: SavedObjectModelDataBackfillFn< + PackagePolicy, + PackagePolicy +> = (packagePolicyDoc) => { + if (packagePolicyDoc.attributes.package?.name !== 'cloud_security_posture') { + return { attributes: packagePolicyDoc.attributes }; + } + + const updatedAttributes = packagePolicyDoc.attributes; + + const gcpPackage = updatedAttributes.inputs.find((input) => input.type === 'cloudbeat/cis_gcp'); + + if (gcpPackage) { + const isGcpAccountTypeExists = gcpPackage.streams[0]?.vars?.hasOwnProperty('gcp.account_type'); + + if (!isGcpAccountTypeExists) { + const migratedPolicy = { 'gcp.account_type': { value: 'single-account', type: 'text' } }; + gcpPackage.streams[0].vars = { ...(gcpPackage.streams[0].vars || {}), ...migratedPolicy }; + } + } + + return { + attributes: updatedAttributes, + }; +}; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/README.md b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/README.md index 81ee936e714fe..949dc743dcefc 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/README.md +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/README.md @@ -6,1719 +6,7 @@ This health API allows users to get health overview of the Detection Engine acro In the future, this API might become helpful for building more Rule Monitoring UIs giving our users more clarity and transparency about the work of the Detection Engine. -## Rule health endpoint +See more info: -🚧 NOTE: this endpoint is **partially implemented**. 🚧 - -```txt -POST /internal/detection_engine/health/_rule -``` - -Get health overview of a rule. Scope: a given detection rule in the current Kibana space. -Returns: - -- health stats at the moment of the API call (rule and its execution summary) -- health stats over a specified period of time ("health interval") -- health stats history within the same interval in the form of a histogram - (the same stats are calculated over each of the discreet sub-intervals of the whole interval) - -Minimal required parameters: - -```json -{ - "rule_id": "d4beff10-f045-11ed-89d8-3b6931af10bc" -} -``` - -Response: - -```json -{ - "timings": { - "requested_at": "2023-05-26T16:09:54.128Z", - "processed_at": "2023-05-26T16:09:54.778Z", - "processing_time_ms": 650 - }, - "parameters": { - "interval": { - "type": "last_day", - "granularity": "hour", - "from": "2023-05-25T16:09:54.128Z", - "to": "2023-05-26T16:09:54.128Z", - "duration": "PT24H" - }, - "rule_id": "d4beff10-f045-11ed-89d8-3b6931af10bc" - }, - "health": { - "stats_at_the_moment": { - "rule": { - "id": "d4beff10-f045-11ed-89d8-3b6931af10bc", - "updated_at": "2023-05-26T15:44:21.689Z", - "updated_by": "elastic", - "created_at": "2023-05-11T21:50:23.830Z", - "created_by": "elastic", - "name": "Test rule", - "tags": ["foo"], - "interval": "1m", - "enabled": true, - "revision": 2, - "description": "-", - "risk_score": 21, - "severity": "low", - "license": "", - "output_index": "", - "meta": { - "from": "6h", - "kibana_siem_app_url": "http://localhost:5601/kbn/app/security" - }, - "author": [], - "false_positives": [], - "from": "now-21660s", - "rule_id": "e46eaaf3-6d81-4cdb-8cbb-b2201a11358b", - "max_signals": 100, - "risk_score_mapping": [], - "severity_mapping": [], - "threat": [], - "to": "now", - "references": [], - "version": 3, - "exceptions_list": [], - "immutable": false, - "related_integrations": [], - "required_fields": [], - "setup": "", - "type": "query", - "language": "kuery", - "index": [ - "apm-*-transaction*", - "auditbeat-*", - "endgame-*", - "filebeat-*", - "logs-*", - "packetbeat-*", - "traces-apm*", - "winlogbeat-*", - "-*elastic-cloud-logs-*", - "foo-*" - ], - "query": "*", - "filters": [], - "actions": [ - { - "group": "default", - "id": "bd59c4e0-f045-11ed-89d8-3b6931af10bc", - "params": { - "body": "Hello world" - }, - "action_type_id": ".webhook", - "uuid": "f8b87eb0-58bb-4d4b-a584-084d44ab847e", - "frequency": { - "summary": true, - "throttle": null, - "notifyWhen": "onActiveAlert" - } - } - ], - "execution_summary": { - "last_execution": { - "date": "2023-05-26T16:09:36.848Z", - "status": "succeeded", - "status_order": 0, - "message": "Rule execution completed successfully", - "metrics": { - "total_search_duration_ms": 2, - "execution_gap_duration_s": 80395 - } - } - } - } - }, - "stats_over_interval": { - "number_of_executions": { - "total": 21, - "by_outcome": { - "succeeded": 20, - "warning": 0, - "failed": 1 - } - }, - "number_of_logged_messages": { - "total": 42, - "by_level": { - "error": 1, - "warn": 0, - "info": 41, - "debug": 0, - "trace": 0 - } - }, - "number_of_detected_gaps": { - "total": 1, - "total_duration_s": 80395 - }, - "schedule_delay_ms": { - "percentiles": { - "1.0": 3061, - "5.0": 3083, - "25.0": 3112, - "50.0": 6049, - "75.0": 6069.5, - "95.0": 100093.79999999986, - "99.0": 207687 - } - }, - "execution_duration_ms": { - "percentiles": { - "1.0": 226, - "5.0": 228.2, - "25.0": 355.5, - "50.0": 422, - "75.0": 447, - "95.0": 677.75, - "99.0": 719 - } - }, - "search_duration_ms": { - "percentiles": { - "1.0": 0, - "5.0": 1.1, - "25.0": 2.75, - "50.0": 7, - "75.0": 13.5, - "95.0": 29.59999999999998, - "99.0": 45 - } - }, - "indexing_duration_ms": { - "percentiles": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 0, - "75.0": 0, - "95.0": 0, - "99.0": 0 - } - }, - "top_errors": [ - { - "count": 1, - "message": "day 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" - } - ], - "top_warnings": [] - }, - "history_over_interval": { - "buckets": [ - { - "timestamp": "2023-05-26T15:00:00.000Z", - "stats": { - "number_of_executions": { - "total": 12, - "by_outcome": { - "succeeded": 11, - "warning": 0, - "failed": 1 - } - }, - "number_of_logged_messages": { - "total": 24, - "by_level": { - "error": 1, - "warn": 0, - "info": 23, - "debug": 0, - "trace": 0 - } - }, - "number_of_detected_gaps": { - "total": 1, - "total_duration_s": 80395 - }, - "schedule_delay_ms": { - "percentiles": { - "1.0": 3106, - "5.0": 3106.8, - "25.0": 3124.5, - "50.0": 6067.5, - "75.0": 9060.5, - "95.0": 188124.59999999971, - "99.0": 207687 - } - }, - "execution_duration_ms": { - "percentiles": { - "1.0": 230, - "5.0": 236.2, - "25.0": 354, - "50.0": 405, - "75.0": 447.5, - "95.0": 563.3999999999999, - "99.0": 576 - } - }, - "search_duration_ms": { - "percentiles": { - "1.0": 0, - "5.0": 0.20000000000000018, - "25.0": 2.5, - "50.0": 5, - "75.0": 14, - "95.0": 42.19999999999996, - "99.0": 45 - } - }, - "indexing_duration_ms": { - "percentiles": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 0, - "75.0": 0, - "95.0": 0, - "99.0": 0 - } - } - } - }, - { - "timestamp": "2023-05-26T16:00:00.000Z", - "stats": { - "number_of_executions": { - "total": 9, - "by_outcome": { - "succeeded": 9, - "warning": 0, - "failed": 0 - } - }, - "number_of_logged_messages": { - "total": 18, - "by_level": { - "error": 0, - "warn": 0, - "info": 18, - "debug": 0, - "trace": 0 - } - }, - "number_of_detected_gaps": { - "total": 0, - "total_duration_s": 0 - }, - "schedule_delay_ms": { - "percentiles": { - "1.0": 3061, - "5.0": 3061, - "25.0": 3104.75, - "50.0": 3115, - "75.0": 6053, - "95.0": 6068, - "99.0": 6068 - } - }, - "execution_duration_ms": { - "percentiles": { - "1.0": 226.00000000000003, - "5.0": 226, - "25.0": 356, - "50.0": 436, - "75.0": 495.5, - "95.0": 719, - "99.0": 719 - } - }, - "search_duration_ms": { - "percentiles": { - "1.0": 2, - "5.0": 2, - "25.0": 5.75, - "50.0": 8, - "75.0": 13.75, - "95.0": 17, - "99.0": 17 - } - }, - "indexing_duration_ms": { - "percentiles": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 0, - "75.0": 0, - "95.0": 0, - "99.0": 0 - } - } - } - } - ] - } - } -} -``` - -## Space health endpoint - -🚧 NOTE: this endpoint is **partially implemented**. 🚧 - -```txt -POST /internal/detection_engine/health/_space -GET /internal/detection_engine/health/_space -``` - -Get health overview of the current Kibana space. Scope: all detection rules in the space. -Returns: - -- health stats at the moment of the API call -- health stats over a specified period of time ("health interval") -- health stats history within the same interval in the form of a histogram - (the same stats are calculated over each of the discreet sub-intervals of the whole interval) - -Minimal required parameters for the `POST` route: empty object. - -```json -{} -``` - -The `GET` route doesn't accept any parameters and uses the default parameters instead: - -- interval: `last_day` -- granularity: `hour` -- debug: `false` - -Response: - -```json -{ - "timings": { - "requested_at": "2023-05-26T16:24:21.628Z", - "processed_at": "2023-05-26T16:24:22.880Z", - "processing_time_ms": 1252 - }, - "parameters": { - "interval": { - "type": "last_day", - "granularity": "hour", - "from": "2023-05-25T16:24:21.628Z", - "to": "2023-05-26T16:24:21.628Z", - "duration": "PT24H" - } - }, - "health": { - "stats_at_the_moment": { - "number_of_rules": { - "all": { - "total": 777, - "enabled": 777, - "disabled": 0 - }, - "by_origin": { - "prebuilt": { - "total": 776, - "enabled": 776, - "disabled": 0 - }, - "custom": { - "total": 1, - "enabled": 1, - "disabled": 0 - } - }, - "by_type": { - "siem.eqlRule": { - "total": 381, - "enabled": 381, - "disabled": 0 - }, - "siem.queryRule": { - "total": 325, - "enabled": 325, - "disabled": 0 - }, - "siem.mlRule": { - "total": 47, - "enabled": 47, - "disabled": 0 - }, - "siem.thresholdRule": { - "total": 18, - "enabled": 18, - "disabled": 0 - }, - "siem.newTermsRule": { - "total": 4, - "enabled": 4, - "disabled": 0 - }, - "siem.indicatorRule": { - "total": 2, - "enabled": 2, - "disabled": 0 - } - }, - "by_outcome": { - "warning": { - "total": 307, - "enabled": 307, - "disabled": 0 - }, - "succeeded": { - "total": 266, - "enabled": 266, - "disabled": 0 - }, - "failed": { - "total": 204, - "enabled": 204, - "disabled": 0 - } - } - } - }, - "stats_over_interval": { - "number_of_executions": { - "total": 5622, - "by_outcome": { - "succeeded": 1882, - "warning": 2129, - "failed": 2120 - } - }, - "number_of_logged_messages": { - "total": 11756, - "by_level": { - "error": 2120, - "warn": 2129, - "info": 7507, - "debug": 0, - "trace": 0 - } - }, - "number_of_detected_gaps": { - "total": 777, - "total_duration_s": 514415894 - }, - "schedule_delay_ms": { - "percentiles": { - "1.0": 216, - "5.0": 3048.5, - "25.0": 3105, - "50.0": 3129, - "75.0": 6112.355119825708, - "95.0": 134006, - "99.0": 195578 - } - }, - "execution_duration_ms": { - "percentiles": { - "1.0": 275, - "5.0": 323.375, - "25.0": 370.80555555555554, - "50.0": 413.1122337092731, - "75.0": 502.25233127864715, - "95.0": 685.8055555555555, - "99.0": 1194.75 - } - }, - "search_duration_ms": { - "percentiles": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 0, - "75.0": 15, - "95.0": 30, - "99.0": 99.44000000000005 - } - }, - "indexing_duration_ms": { - "percentiles": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 0, - "75.0": 0, - "95.0": 0, - "99.0": 0 - } - }, - "top_errors": [ - { - "count": 1202, - "message": "An error occurred during rule execution message verification_exception" - }, - { - "count": 777, - "message": "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" - }, - { - "count": 3, - "message": "An error occurred during rule execution message rare_error_code missing" - }, - { - "count": 3, - "message": "An error occurred during rule execution message v3_windows_anomalous_path_activity missing" - }, - { - "count": 3, - "message": "An error occurred during rule execution message v3_windows_rare_user_type10_remote_login missing" - } - ], - "top_warnings": [ - { - "count": 2129, - "message": "This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching was found This warning will continue to appear until matching index is created or this rule is disabled" - } - ] - }, - "history_over_interval": { - "buckets": [ - { - "timestamp": "2023-05-26T15:00:00.000Z", - "stats": { - "number_of_executions": { - "total": 2245, - "by_outcome": { - "succeeded": 566, - "warning": 849, - "failed": 1336 - } - }, - "number_of_logged_messages": { - "total": 4996, - "by_level": { - "error": 1336, - "warn": 849, - "info": 2811, - "debug": 0, - "trace": 0 - } - }, - "number_of_detected_gaps": { - "total": 777, - "total_duration_s": 514415894 - }, - "schedule_delay_ms": { - "percentiles": { - "1.0": 256, - "5.0": 3086.9722222222217, - "25.0": 3133, - "50.0": 6126, - "75.0": 59484.25, - "95.0": 179817.25, - "99.0": 202613 - } - }, - "execution_duration_ms": { - "percentiles": { - "1.0": 280.6, - "5.0": 327.7, - "25.0": 371.5208333333333, - "50.0": 415.6190476190476, - "75.0": 505.7642857142857, - "95.0": 740.4375, - "99.0": 1446.1500000000005 - } - }, - "search_duration_ms": { - "percentiles": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 0, - "75.0": 8, - "95.0": 25, - "99.0": 46 - } - }, - "indexing_duration_ms": { - "percentiles": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 0, - "75.0": 0, - "95.0": 0, - "99.0": 0 - } - } - } - }, - { - "timestamp": "2023-05-26T16:00:00.000Z", - "stats": { - "number_of_executions": { - "total": 3363, - "by_outcome": { - "succeeded": 1316, - "warning": 1280, - "failed": 784 - } - }, - "number_of_logged_messages": { - "total": 6760, - "by_level": { - "error": 784, - "warn": 1280, - "info": 4696, - "debug": 0, - "trace": 0 - } - }, - "number_of_detected_gaps": { - "total": 0, - "total_duration_s": 0 - }, - "schedule_delay_ms": { - "percentiles": { - "1.0": 207, - "5.0": 3042, - "25.0": 3098.46511627907, - "50.0": 3112, - "75.0": 3145.2820512820517, - "95.0": 6100.571428571428, - "99.0": 6123 - } - }, - "execution_duration_ms": { - "percentiles": { - "1.0": 275, - "5.0": 319.85714285714283, - "25.0": 370.0357142857143, - "50.0": 410.79999229108853, - "75.0": 500.7692307692308, - "95.0": 675, - "99.0": 781.3999999999996 - } - }, - "search_duration_ms": { - "percentiles": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 9, - "75.0": 17.555555555555557, - "95.0": 34, - "99.0": 110.5 - } - }, - "indexing_duration_ms": { - "percentiles": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 0, - "75.0": 0, - "95.0": 0, - "99.0": 0 - } - } - } - } - ] - } - } -} -``` - -## Cluster health endpoint - -🚧 NOTE: this endpoint is **not implemented**. 🚧 - -```txt -POST /internal/detection_engine/health/_cluster -GET /internal/detection_engine/health/_cluster -``` - -Minimal required parameters for the `POST` route: empty object. - -```json -{} -``` - -The `GET` route doesn't accept any parameters and uses the default parameters instead: - -- interval: `last_day` -- granularity: `hour` -- debug: `false` - -Response: - -```json -{ - "message": "Not implemented", - "timings": { - "requested_at": "2023-05-26T16:32:01.878Z", - "processed_at": "2023-05-26T16:32:01.881Z", - "processing_time_ms": 3 - }, - "parameters": { - "interval": { - "type": "last_week", - "granularity": "hour", - "from": "2023-05-19T16:32:01.878Z", - "to": "2023-05-26T16:32:01.878Z", - "duration": "PT168H" - } - }, - "health": { - "stats_at_the_moment": { - "number_of_rules": { - "all": { - "total": 0, - "enabled": 0, - "disabled": 0 - }, - "by_origin": { - "prebuilt": { - "total": 0, - "enabled": 0, - "disabled": 0 - }, - "custom": { - "total": 0, - "enabled": 0, - "disabled": 0 - } - }, - "by_type": {}, - "by_outcome": {} - } - }, - "stats_over_interval": { - "message": "Not implemented" - }, - "history_over_interval": { - "buckets": [] - } - } -} -``` - -## Optional parameters - -All the three endpoints accept optional `interval` and `debug` request parameters. - -### Health interval - -You can change the interval over which the health stats will be calculated. If you don't specify it, by default health stats will be calculated over the last day with the granularity of 1 hour. - -```json -{ - "interval": { - "type": "last_week", - "granularity": "day" - } -} -``` - -You can also specify a custom date range with exact interval bounds. - -```json -{ - "interval": { - "type": "custom_range", - "granularity": "minute", - "from": "2023-05-20T16:24:21.628Z", - "to": "2023-05-26T16:24:21.628Z" - } -} -``` - -Please keep in mind that requesting large intervals with small granularity can generate substantial load on the system and enormous API responses. - -### Debug mode - -You can also include various debug information in the response, such as queries and aggregations sent to Elasticsearch and response received from it. - -```json -{ - "debug": true -} -``` - -In the response you will find something like that: - -```json -{ - "health": { - "debug": { - "rulesClient": { - "request": { - "aggs": { - "rulesByEnabled": { - "terms": { - "field": "alert.attributes.enabled" - } - }, - "rulesByOrigin": { - "terms": { - "field": "alert.attributes.params.immutable" - }, - "aggs": { - "rulesByEnabled": { - "terms": { - "field": "alert.attributes.enabled" - } - } - } - }, - "rulesByType": { - "terms": { - "field": "alert.attributes.alertTypeId" - }, - "aggs": { - "rulesByEnabled": { - "terms": { - "field": "alert.attributes.enabled" - } - } - } - }, - "rulesByOutcome": { - "terms": { - "field": "alert.attributes.lastRun.outcome" - }, - "aggs": { - "rulesByEnabled": { - "terms": { - "field": "alert.attributes.enabled" - } - } - } - } - } - }, - "response": { - "aggregations": { - "rulesByOutcome": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": "warning", - "doc_count": 307, - "rulesByEnabled": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": 1, - "key_as_string": "true", - "doc_count": 307 - } - ] - } - }, - { - "key": "succeeded", - "doc_count": 266, - "rulesByEnabled": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": 1, - "key_as_string": "true", - "doc_count": 266 - } - ] - } - }, - { - "key": "failed", - "doc_count": 204, - "rulesByEnabled": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": 1, - "key_as_string": "true", - "doc_count": 204 - } - ] - } - } - ] - }, - "rulesByType": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": "siem.eqlRule", - "doc_count": 381, - "rulesByEnabled": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": 1, - "key_as_string": "true", - "doc_count": 381 - } - ] - } - }, - { - "key": "siem.queryRule", - "doc_count": 325, - "rulesByEnabled": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": 1, - "key_as_string": "true", - "doc_count": 325 - } - ] - } - }, - { - "key": "siem.mlRule", - "doc_count": 47, - "rulesByEnabled": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": 1, - "key_as_string": "true", - "doc_count": 47 - } - ] - } - }, - { - "key": "siem.thresholdRule", - "doc_count": 18, - "rulesByEnabled": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": 1, - "key_as_string": "true", - "doc_count": 18 - } - ] - } - }, - { - "key": "siem.newTermsRule", - "doc_count": 4, - "rulesByEnabled": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": 1, - "key_as_string": "true", - "doc_count": 4 - } - ] - } - }, - { - "key": "siem.indicatorRule", - "doc_count": 2, - "rulesByEnabled": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": 1, - "key_as_string": "true", - "doc_count": 2 - } - ] - } - } - ] - }, - "rulesByOrigin": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": "true", - "doc_count": 776, - "rulesByEnabled": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": 1, - "key_as_string": "true", - "doc_count": 776 - } - ] - } - }, - { - "key": "false", - "doc_count": 1, - "rulesByEnabled": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": 1, - "key_as_string": "true", - "doc_count": 1 - } - ] - } - } - ] - }, - "rulesByEnabled": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": 1, - "key_as_string": "true", - "doc_count": 777 - } - ] - } - } - } - }, - "eventLog": { - "request": { - "aggs": { - "totalExecutions": { - "cardinality": { - "field": "kibana.alert.rule.execution.uuid" - } - }, - "executeEvents": { - "filter": { - "term": { - "event.action": "execute" - } - }, - "aggs": { - "executionDurationMs": { - "percentiles": { - "field": "kibana.alert.rule.execution.metrics.total_run_duration_ms", - "missing": 0, - "percents": [1, 5, 25, 50, 75, 95, 99] - } - }, - "scheduleDelayNs": { - "percentiles": { - "field": "kibana.task.schedule_delay", - "missing": 0, - "percents": [1, 5, 25, 50, 75, 95, 99] - } - } - } - }, - "statusChangeEvents": { - "filter": { - "bool": { - "filter": [ - { - "term": { - "event.action": "status-change" - } - } - ], - "must_not": [ - { - "terms": { - "kibana.alert.rule.execution.status": ["running", "going to run"] - } - } - ] - } - }, - "aggs": { - "executionsByStatus": { - "terms": { - "field": "kibana.alert.rule.execution.status" - } - } - } - }, - "executionMetricsEvents": { - "filter": { - "term": { - "event.action": "execution-metrics" - } - }, - "aggs": { - "gaps": { - "filter": { - "exists": { - "field": "kibana.alert.rule.execution.metrics.execution_gap_duration_s" - } - }, - "aggs": { - "totalGapDurationS": { - "sum": { - "field": "kibana.alert.rule.execution.metrics.execution_gap_duration_s" - } - } - } - }, - "searchDurationMs": { - "percentiles": { - "field": "kibana.alert.rule.execution.metrics.total_search_duration_ms", - "missing": 0, - "percents": [1, 5, 25, 50, 75, 95, 99] - } - }, - "indexingDurationMs": { - "percentiles": { - "field": "kibana.alert.rule.execution.metrics.total_indexing_duration_ms", - "missing": 0, - "percents": [1, 5, 25, 50, 75, 95, 99] - } - } - } - }, - "messageContainingEvents": { - "filter": { - "terms": { - "event.action": ["status-change", "message"] - } - }, - "aggs": { - "messagesByLogLevel": { - "terms": { - "field": "log.level" - } - }, - "errors": { - "filter": { - "term": { - "log.level": "error" - } - }, - "aggs": { - "topErrors": { - "categorize_text": { - "field": "message", - "size": 5, - "similarity_threshold": 99 - } - } - } - }, - "warnings": { - "filter": { - "term": { - "log.level": "warn" - } - }, - "aggs": { - "topWarnings": { - "categorize_text": { - "field": "message", - "size": 5, - "similarity_threshold": 99 - } - } - } - } - } - }, - "statsHistory": { - "date_histogram": { - "field": "@timestamp", - "calendar_interval": "hour" - }, - "aggs": { - "totalExecutions": { - "cardinality": { - "field": "kibana.alert.rule.execution.uuid" - } - }, - "executeEvents": { - "filter": { - "term": { - "event.action": "execute" - } - }, - "aggs": { - "executionDurationMs": { - "percentiles": { - "field": "kibana.alert.rule.execution.metrics.total_run_duration_ms", - "missing": 0, - "percents": [1, 5, 25, 50, 75, 95, 99] - } - }, - "scheduleDelayNs": { - "percentiles": { - "field": "kibana.task.schedule_delay", - "missing": 0, - "percents": [1, 5, 25, 50, 75, 95, 99] - } - } - } - }, - "statusChangeEvents": { - "filter": { - "bool": { - "filter": [ - { - "term": { - "event.action": "status-change" - } - } - ], - "must_not": [ - { - "terms": { - "kibana.alert.rule.execution.status": ["running", "going to run"] - } - } - ] - } - }, - "aggs": { - "executionsByStatus": { - "terms": { - "field": "kibana.alert.rule.execution.status" - } - } - } - }, - "executionMetricsEvents": { - "filter": { - "term": { - "event.action": "execution-metrics" - } - }, - "aggs": { - "gaps": { - "filter": { - "exists": { - "field": "kibana.alert.rule.execution.metrics.execution_gap_duration_s" - } - }, - "aggs": { - "totalGapDurationS": { - "sum": { - "field": "kibana.alert.rule.execution.metrics.execution_gap_duration_s" - } - } - } - }, - "searchDurationMs": { - "percentiles": { - "field": "kibana.alert.rule.execution.metrics.total_search_duration_ms", - "missing": 0, - "percents": [1, 5, 25, 50, 75, 95, 99] - } - }, - "indexingDurationMs": { - "percentiles": { - "field": "kibana.alert.rule.execution.metrics.total_indexing_duration_ms", - "missing": 0, - "percents": [1, 5, 25, 50, 75, 95, 99] - } - } - } - }, - "messageContainingEvents": { - "filter": { - "terms": { - "event.action": ["status-change", "message"] - } - }, - "aggs": { - "messagesByLogLevel": { - "terms": { - "field": "log.level" - } - } - } - } - } - } - } - }, - "response": { - "aggregations": { - "statsHistory": { - "buckets": [ - { - "key_as_string": "2023-05-26T15:00:00.000Z", - "key": 1685113200000, - "doc_count": 11388, - "statusChangeEvents": { - "doc_count": 2751, - "executionsByStatus": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": "failed", - "doc_count": 1336 - }, - { - "key": "partial failure", - "doc_count": 849 - }, - { - "key": "succeeded", - "doc_count": 566 - } - ] - } - }, - "totalExecutions": { - "value": 2245 - }, - "messageContainingEvents": { - "doc_count": 4996, - "messagesByLogLevel": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": "info", - "doc_count": 2811 - }, - { - "key": "error", - "doc_count": 1336 - }, - { - "key": "warn", - "doc_count": 849 - } - ] - } - }, - "executeEvents": { - "doc_count": 2245, - "scheduleDelayNs": { - "values": { - "1.0": 256000000, - "5.0": 3086972222.222222, - "25.0": 3133000000, - "50.0": 6126000000, - "75.0": 59484250000, - "95.0": 179817250000, - "99.0": 202613000000 - } - }, - "executionDurationMs": { - "values": { - "1.0": 280.6, - "5.0": 327.7, - "25.0": 371.5208333333333, - "50.0": 415.6190476190476, - "75.0": 505.575, - "95.0": 740.4375, - "99.0": 1446.1500000000005 - } - } - }, - "executionMetricsEvents": { - "doc_count": 1902, - "searchDurationMs": { - "values": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 0, - "75.0": 8, - "95.0": 25, - "99.0": 46 - } - }, - "gaps": { - "doc_count": 777, - "totalGapDurationS": { - "value": 514415894 - } - }, - "indexingDurationMs": { - "values": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 0, - "75.0": 0, - "95.0": 0, - "99.0": 0 - } - } - } - }, - { - "key_as_string": "2023-05-26T16:00:00.000Z", - "key": 1685116800000, - "doc_count": 28325, - "statusChangeEvents": { - "doc_count": 6126, - "executionsByStatus": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": "succeeded", - "doc_count": 2390 - }, - { - "key": "partial failure", - "doc_count": 2305 - }, - { - "key": "failed", - "doc_count": 1431 - } - ] - } - }, - "totalExecutions": { - "value": 6170 - }, - "messageContainingEvents": { - "doc_count": 12252, - "messagesByLogLevel": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": "info", - "doc_count": 8516 - }, - { - "key": "warn", - "doc_count": 2305 - }, - { - "key": "error", - "doc_count": 1431 - } - ] - } - }, - "executeEvents": { - "doc_count": 6126, - "scheduleDelayNs": { - "values": { - "1.0": 193000000, - "5.0": 3017785185.1851854, - "25.0": 3086000000, - "50.0": 3105877192.982456, - "75.0": 3134645161.290323, - "95.0": 6081772222.222222, - "99.0": 6122000000 - } - }, - "executionDurationMs": { - "values": { - "1.0": 275.17333333333335, - "5.0": 324.8014285714285, - "25.0": 377.0752688172043, - "50.0": 431, - "75.0": 532.3870967741935, - "95.0": 720.6761904761904, - "99.0": 922.6799999999985 - } - } - }, - "executionMetricsEvents": { - "doc_count": 3821, - "searchDurationMs": { - "values": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 9.8, - "75.0": 18, - "95.0": 40.17499999999999, - "99.0": 124 - } - }, - "gaps": { - "doc_count": 0, - "totalGapDurationS": { - "value": 0 - } - }, - "indexingDurationMs": { - "values": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 0, - "75.0": 0, - "95.0": 0, - "99.0": 0 - } - } - } - } - ] - }, - "statusChangeEvents": { - "doc_count": 8877, - "executionsByStatus": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": "partial failure", - "doc_count": 3154 - }, - { - "key": "succeeded", - "doc_count": 2956 - }, - { - "key": "failed", - "doc_count": 2767 - } - ] - } - }, - "totalExecutions": { - "value": 8455 - }, - "messageContainingEvents": { - "doc_count": 17248, - "messagesByLogLevel": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": "info", - "doc_count": 11327 - }, - { - "key": "warn", - "doc_count": 3154 - }, - { - "key": "error", - "doc_count": 2767 - } - ] - }, - "warnings": { - "doc_count": 3154, - "topWarnings": { - "buckets": [ - { - "doc_count": 3154, - "key": "This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching was found This warning will continue to appear until matching index is created or this rule is disabled", - "regex": ".*?This.+?rule.+?is.+?attempting.+?to.+?query.+?data.+?from.+?Elasticsearch.+?indices.+?listed.+?in.+?the.+?Index.+?pattern.+?section.+?of.+?the.+?rule.+?definition.+?however.+?no.+?index.+?matching.+?was.+?found.+?This.+?warning.+?will.+?continue.+?to.+?appear.+?until.+?matching.+?index.+?is.+?created.+?or.+?this.+?rule.+?is.+?disabled.*?", - "max_matching_length": 342 - } - ] - } - }, - "errors": { - "doc_count": 2767, - "topErrors": { - "buckets": [ - { - "doc_count": 1802, - "key": "An error occurred during rule execution message verification_exception", - "regex": ".*?An.+?error.+?occurred.+?during.+?rule.+?execution.+?message.+?verification_exception.*?", - "max_matching_length": 2064 - }, - { - "doc_count": 777, - "key": "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", - "regex": ".*?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.*?", - "max_matching_length": 216 - }, - { - "doc_count": 4, - "key": "An error occurred during rule execution message rare_error_code missing", - "regex": ".*?An.+?error.+?occurred.+?during.+?rule.+?execution.+?message.+?rare_error_code.+?missing.*?", - "max_matching_length": 82 - }, - { - "doc_count": 4, - "key": "An error occurred during rule execution message v3_windows_anomalous_path_activity missing", - "regex": ".*?An.+?error.+?occurred.+?during.+?rule.+?execution.+?message.+?v3_windows_anomalous_path_activity.+?missing.*?", - "max_matching_length": 103 - }, - { - "doc_count": 4, - "key": "An error occurred during rule execution message v3_windows_rare_user_type10_remote_login missing", - "regex": ".*?An.+?error.+?occurred.+?during.+?rule.+?execution.+?message.+?v3_windows_rare_user_type10_remote_login.+?missing.*?", - "max_matching_length": 110 - } - ] - } - } - }, - "executeEvents": { - "doc_count": 8371, - "scheduleDelayNs": { - "values": { - "1.0": 206000000, - "5.0": 3027000000, - "25.0": 3092000000, - "50.0": 3116000000, - "75.0": 3278666666.6666665, - "95.0": 99656950000, - "99.0": 186632790000 - } - }, - "executionDurationMs": { - "values": { - "1.0": 275.5325, - "5.0": 326.07857142857137, - "25.0": 375.68969144460027, - "50.0": 427, - "75.0": 526.2948717948718, - "95.0": 727.2480952380952, - "99.0": 1009.5299999999934 - } - } - }, - "executionMetricsEvents": { - "doc_count": 5723, - "searchDurationMs": { - "values": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 4, - "75.0": 16, - "95.0": 34.43846153846145, - "99.0": 116.51333333333302 - } - }, - "gaps": { - "doc_count": 777, - "totalGapDurationS": { - "value": 514415894 - } - }, - "indexingDurationMs": { - "values": { - "1.0": 0, - "5.0": 0, - "25.0": 0, - "50.0": 0, - "75.0": 0, - "95.0": 0, - "99.0": 0 - } - } - } - } - } - } - } - } -} -``` +- [Detection Engine health data](./health_data.md) +- [Detection Engine health endpoints](./health_endpoints.md) diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/get_cluster_health/get_cluster_health_route.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/get_cluster_health/get_cluster_health_route.ts index dafa1e4a4a1df..fd2c07352dc95 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/get_cluster_health/get_cluster_health_route.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/get_cluster_health/get_cluster_health_route.ts @@ -51,9 +51,6 @@ export interface GetClusterHealthRequest { * Response body of the endpoint. */ export interface GetClusterHealthResponse { - // TODO: https://github.com/elastic/kibana/issues/125642 Implement the endpoint and remove the `message` property - message: 'Not implemented'; - /** * Request processing times and durations. */ diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/health_data.md b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/health_data.md new file mode 100644 index 0000000000000..052d81d9ffe6a --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/health_data.md @@ -0,0 +1,152 @@ +# Detection Engine health data + +This document describes the data (state, aggregated metrics, aggregated logs, and other data) we return or are planning to return from the Detection Engine health API. + +Legend for the tables below: + +- ✅ - implemented +- 🚧 - in development +- 👍 - planned +- ❓ - maybe planned +- ❌ - not planned or applicable +- TED - "total, enabled, disabled" (applies to the "number of rules" state or metrics) + +## Health overview + +Health overview data is intended to give a broad, high-level picture of detection rules' health and performance. It is returned from the following three endpoints: + +- **Cluster health**: `/internal/detection_engine/health/_cluster` + - Returns: health overview of the whole cluster + - Scope: all detection rules in all Kibana spaces +- **Space health**: `/internal/detection_engine/health/_space` + - Returns: health overview of a given Kibana space + - Scope: all detection rules in the space +- **Rule health**: `/internal/detection_engine/health/_rule` + - Returns: health overview of a given rule + - Scope: a given detection rule in a given Kibana space + +All three endpoints accept a datetime interval parameter (**"health interval"**) over which they calculate health metrics, such as the number of rule executions happened during this interval. + +All three endpoints return: + +- **Health state at the moment of the API call**. Applies to the "now" moment. Can answer the following type of questions: + - "What rules do I have now?" + - "Are my rules healthy now? What are their last execution statuses?" + - "Are my rules running fast enough now? What are their last execution durations?" +- **Health stats over the specified "health interval"**. Can answer the following type of questions: + - "Overall, were my rules healthy and performant last 24 hours?" + - "How many rule executions in total happened last 24 hours?" + - "What were typical rule execution errors and warnings last 24 hours?" + - "How fast, on average, were my rules executing during this timeframe?", where "on average" can be, for example, a median, or a 95th percentile of the "execution duration" metric. +- **Health history over the specified "health interval"**. Shows dynamics of change of health stats, calculated within the "health interval" in the form of a histogram (the same stats are calculated over each of the discreet sub-intervals of the whole interval). Can answer the following type of questions: + - "How was my rules' health and performance changing during the last 24 hours? Show me stats calculated for each hour." + - "How many rule executions happened every hour of the last 24 hours?" + - "What were typical rule execution errors and warnings every hour of the last 24 hours?" + - "How fast, on average, were my rules executing every hour of the last 24 hours?" + +Below is the data we return or could return from these three endpoints. + +### Health overview: state at the moment of the API call + +ℹ️ When we say "rule" we mean `{ space_id, rule_id, rule_name? }`. + +| Data | `_cluster` | `_space` | `_rule` | +| ------------------------------------------------------------------------ | ---------- | -------- | ------- | +| **RULE OBJECT** | | | | +| rule object with all attributes | ❌ | ❌ | ✅ | +| rule's execution summary (contains last execution status and metrics) | ❌ | ❌ | ✅ | +| rule's exceptions | ❌ | ❌ | ❓ | +| rule's actions | ❌ | ❌ | ✅ | +| rule's action connectors | ❌ | ❌ | ❓ | +| **SYSTEM** | | | | +| number of ES nodes: total | 👍 | ❌ | ❌ | +| number of ES nodes of each role | 👍 | ❌ | ❌ | +| number of Kibana instances: total | 👍 | ❌ | ❌ | +| number of Kibana instances of each role | 👍 | ❌ | ❌ | +| number of Kibana spaces: total | 👍 | ❌ | ❌ | +| number of Kibana spaces: w/ alerting rules (of all types) | 👍 | ❌ | ❌ | +| number of Kibana spaces: w/ detection rules | 👍 | ❌ | ❌ | +| **AGGREGATED RULES** | | | | +| number of all rules: TED | ✅ | ✅ | ❌ | +| number of prebuilt rules: TED | ✅ | ✅ | ❌ | +| number of custom rules: TED | ✅ | ✅ | ❌ | +| number of rules of each type: TED | ✅ | ✅ | ❌ | +| number of prebuilt rules of each type: TED | 👍 | 👍 | ❌ | +| number of custom rules of each type: TED | 👍 | 👍 | ❌ | +| number of rules with exceptions: TED | ❓ | ❓ | ❌ | +| number of rules with notification actions: TED | ❓ | ❓ | ❌ | +| number of rules with legacy notification actions: TED | ❓ | ❓ | ❌ | +| number of rules with response actions: TED | ❓ | ❓ | ❌ | +| **AGGREGATED LAST EXECUTION RESULTS** | | | | +| number of rules by execution outcome ("succeeded", "warning", "failed") | ✅ | ✅ | ❌ | +| top X most common "failed" outcomes | 👍 | 👍 | ❌ | +| top X most common "warning" outcomes | 👍 | 👍 | ❌ | +| number of rules for each most common execution outcome | 👍 | 👍 | ❌ | +| rules for each most common execution outcome | 👍 | 👍 | ❌ | +| top X rules by execution duration (desc) | 👍 | 👍 | ❌ | +| top X rules by search duration (desc) | 👍 | 👍 | ❌ | +| top X rules by indexing duration (desc) | 👍 | 👍 | ❌ | +| top X rules by detected gap duration (desc) | 👍 | 👍 | ❌ | +| top X rules by schedule delay aka drift (desc) | 👍 | 👍 | ❌ | +| top X rules by number of shards queried (desc) | 👍 | 👍 | ❌ | +| top X rules by number of shards queried in a particular data tier (desc) | 👍 | 👍 | ❌ | + +### Health overview: stats and history over interval + +ℹ️ [S, H] == [Stats over interval, History over interval] + +| Data | `_cluster` [S,H] | `_space` [S,H] | `_rule` [S,H] | +| ------------------------------------------------------------------- | ---------------- | -------------- | ------------- | +| **AGGREGATED EXECUTION STATUSES** | | | | +| number of executions: total | [✅, ✅] | [✅, ✅] | [✅, ✅] | +| number of executions: by outcome ("succeeded", "warning", "failed") | [✅, ✅] | [✅, ✅] | [✅, ✅] | +| top X "failed" outcomes (status messages) | [👍, ❓] | [👍, ❓] | [👍, ❓] | +| top X "warning" outcomes (status messages) | [👍, ❓] | [👍, ❓] | [👍, ❓] | +| **AGGREGATED EXECUTION METRICS** | | | | +| aggregated execution duration (percentiles: 50, 95, 99, 99.9) | [✅, ✅] | [✅, ✅] | [✅, ✅] | +| aggregated search duration (percentiles: 50, 95, 99, 99.9) | [✅, ✅] | [✅, ✅] | [✅, ✅] | +| aggregated indexing duration (percentiles: 50, 95, 99, 99.9) | [✅, ✅] | [✅, ✅] | [✅, ✅] | +| aggregated schedule delay aka drift (percentiles: 50, 95, 99, 99.9) | [✅, ✅] | [✅, ✅] | [✅, ✅] | +| detected gaps: total number | [✅, ✅] | [✅, ✅] | [✅, ✅] | +| detected gaps: total duration | [✅, ✅] | [✅, ✅] | [✅, ✅] | +| number of detected alerts (those we generated in memory) | [👍, 👍] | [👍, 👍] | [👍, 👍] | +| number of created alerts (those we wrote to the `.alerts-*` index) | [👍, 👍] | [👍, 👍] | [👍, 👍] | +| number of not created alerts because of cirquit breaker | [👍, 👍] | [👍, 👍] | [👍, 👍] | +| number of executions when cirquit breaker was hit | [👍, 👍] | [👍, 👍] | [👍, 👍] | +| number of triggered actions | [❓, ❓] | [❓, ❓] | [❓, ❓] | +| **AGGREGATED EXECUTION LOGS (MESSAGE EVENTS)** | | | | +| number of logged messages: total | [✅, ✅] | [✅, ✅] | [✅, ✅] | +| number of logged messages: by log level | [✅, ✅] | [✅, ✅] | [✅, ✅] | +| top X errors (messages with log level "error") | [✅, ❓] | [✅, ❓] | [✅, ❓] | +| top X warnings (messages with log level "warn") | [✅, ❓] | [✅, ❓] | [✅, ❓] | +| top X error codes (we don't have error codes in our logs yet) | | | | + +## Health details + +Detailed health data can be used for digging deeper into detection rules' health and performance, when the overall picture is clear. It should be returned from dedicated endpoints. Each kind of details we can calculate within either of two scopes: + +- The whole cluster, i.e. all Kibana spaces. +- A given Kibana space. + +**NOTE**: As of now, we don't have any endpoints that would return detailed data. + +ℹ️ When we say "rule" we mean `{ space_id, rule_id, rule_name? }`. + +| Data | Scope: cluster | Scope: space | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------ | +| **RULES SORTED BY EXECUTION STATUSES** | | | +| top X rules by number of execution outcomes: total (desc) - in other words, rules that were running most often within a given interval | 👍 | 👍 | +| top X rules by number of execution outcomes: by outcome (desc) - in other words, rules that were failing/succeeding/etc most often within a given interval | 👍 | 👍 | +| **RULES SORTED BY EXECUTION METRICS** | | | +| top X rules by execution duration (percentile, desc) | 👍 | 👍 | +| top X rules by search duration (percentile, desc) | 👍 | 👍 | +| top X rules by indexing duration (percentile, desc) | 👍 | 👍 | +| top X rules by detected gap duration (percentile, desc) | 👍 | 👍 | +| top X rules by schedule delay aka drift (percentile, desc) | 👍 | 👍 | +| top X rules by number of shards queried (percentile, desc) | 👍 | 👍 | +| top X rules by number of shards queried in a particular data tier (percentile, desc) | 👍 | 👍 | +| top X rules that are consuming the most total execution time - summing execution time over the executions for that rule, so it accounts for rules that are running more often | 👍 | 👍 | +| **RULES SORTED BY EXECUTION LOGS (MESSAGE EVENTS)** | | | +| top X rules by number of logged messages (log level, desc) - errors and warnings are most interesting | 👍 | 👍 | +| **SOURCE INDICES / DATA QUALITY** | | | +| all rules that are querying indices with future timestamps + the actual index names with future timestamps in them (the API would need to check all rule's index patterns and data views) | 👍 | 👍 | diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/health_endpoints.md b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/health_endpoints.md new file mode 100644 index 0000000000000..c82153a0dad4e --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/health_endpoints.md @@ -0,0 +1,1944 @@ +# Detection Engine health endpoints + +## Rule health endpoint + +🚧 NOTE: this endpoint is **partially implemented**. 🚧 + +```txt +POST /internal/detection_engine/health/_rule +``` + +Get health overview of a rule. Scope: a given detection rule in the current Kibana space. +Returns: + +- health stats at the moment of the API call (rule and its execution summary) +- health stats over a specified period of time ("health interval") +- health stats history within the same interval in the form of a histogram + (the same stats are calculated over each of the discreet sub-intervals of the whole interval) + +Minimal required parameters: + +```json +{ + "rule_id": "d4beff10-f045-11ed-89d8-3b6931af10bc" +} +``` + +Response: + +```json +{ + "timings": { + "requested_at": "2023-05-26T16:09:54.128Z", + "processed_at": "2023-05-26T16:09:54.778Z", + "processing_time_ms": 650 + }, + "parameters": { + "interval": { + "type": "last_day", + "granularity": "hour", + "from": "2023-05-25T16:09:54.128Z", + "to": "2023-05-26T16:09:54.128Z", + "duration": "PT24H" + }, + "rule_id": "d4beff10-f045-11ed-89d8-3b6931af10bc" + }, + "health": { + "state_at_the_moment": { + "rule": { + "id": "d4beff10-f045-11ed-89d8-3b6931af10bc", + "updated_at": "2023-05-26T15:44:21.689Z", + "updated_by": "elastic", + "created_at": "2023-05-11T21:50:23.830Z", + "created_by": "elastic", + "name": "Test rule", + "tags": ["foo"], + "interval": "1m", + "enabled": true, + "revision": 2, + "description": "-", + "risk_score": 21, + "severity": "low", + "license": "", + "output_index": "", + "meta": { + "from": "6h", + "kibana_siem_app_url": "http://localhost:5601/kbn/app/security" + }, + "author": [], + "false_positives": [], + "from": "now-21660s", + "rule_id": "e46eaaf3-6d81-4cdb-8cbb-b2201a11358b", + "max_signals": 100, + "risk_score_mapping": [], + "severity_mapping": [], + "threat": [], + "to": "now", + "references": [], + "version": 3, + "exceptions_list": [], + "immutable": false, + "related_integrations": [], + "required_fields": [], + "setup": "", + "type": "query", + "language": "kuery", + "index": [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "traces-apm*", + "winlogbeat-*", + "-*elastic-cloud-logs-*", + "foo-*" + ], + "query": "*", + "filters": [], + "actions": [ + { + "group": "default", + "id": "bd59c4e0-f045-11ed-89d8-3b6931af10bc", + "params": { + "body": "Hello world" + }, + "action_type_id": ".webhook", + "uuid": "f8b87eb0-58bb-4d4b-a584-084d44ab847e", + "frequency": { + "summary": true, + "throttle": null, + "notifyWhen": "onActiveAlert" + } + } + ], + "execution_summary": { + "last_execution": { + "date": "2023-05-26T16:09:36.848Z", + "status": "succeeded", + "status_order": 0, + "message": "Rule execution completed successfully", + "metrics": { + "total_search_duration_ms": 2, + "execution_gap_duration_s": 80395 + } + } + } + } + }, + "stats_over_interval": { + "number_of_executions": { + "total": 21, + "by_outcome": { + "succeeded": 20, + "warning": 0, + "failed": 1 + } + }, + "number_of_logged_messages": { + "total": 42, + "by_level": { + "error": 1, + "warn": 0, + "info": 41, + "debug": 0, + "trace": 0 + } + }, + "number_of_detected_gaps": { + "total": 1, + "total_duration_s": 80395 + }, + "schedule_delay_ms": { + "percentiles": { + "1.0": 3061, + "5.0": 3083, + "25.0": 3112, + "50.0": 6049, + "75.0": 6069.5, + "95.0": 100093.79999999986, + "99.0": 207687 + } + }, + "execution_duration_ms": { + "percentiles": { + "1.0": 226, + "5.0": 228.2, + "25.0": 355.5, + "50.0": 422, + "75.0": 447, + "95.0": 677.75, + "99.0": 719 + } + }, + "search_duration_ms": { + "percentiles": { + "1.0": 0, + "5.0": 1.1, + "25.0": 2.75, + "50.0": 7, + "75.0": 13.5, + "95.0": 29.59999999999998, + "99.0": 45 + } + }, + "indexing_duration_ms": { + "percentiles": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 0, + "75.0": 0, + "95.0": 0, + "99.0": 0 + } + }, + "top_errors": [ + { + "count": 1, + "message": "day 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" + } + ], + "top_warnings": [] + }, + "history_over_interval": { + "buckets": [ + { + "timestamp": "2023-05-26T15:00:00.000Z", + "stats": { + "number_of_executions": { + "total": 12, + "by_outcome": { + "succeeded": 11, + "warning": 0, + "failed": 1 + } + }, + "number_of_logged_messages": { + "total": 24, + "by_level": { + "error": 1, + "warn": 0, + "info": 23, + "debug": 0, + "trace": 0 + } + }, + "number_of_detected_gaps": { + "total": 1, + "total_duration_s": 80395 + }, + "schedule_delay_ms": { + "percentiles": { + "1.0": 3106, + "5.0": 3106.8, + "25.0": 3124.5, + "50.0": 6067.5, + "75.0": 9060.5, + "95.0": 188124.59999999971, + "99.0": 207687 + } + }, + "execution_duration_ms": { + "percentiles": { + "1.0": 230, + "5.0": 236.2, + "25.0": 354, + "50.0": 405, + "75.0": 447.5, + "95.0": 563.3999999999999, + "99.0": 576 + } + }, + "search_duration_ms": { + "percentiles": { + "1.0": 0, + "5.0": 0.20000000000000018, + "25.0": 2.5, + "50.0": 5, + "75.0": 14, + "95.0": 42.19999999999996, + "99.0": 45 + } + }, + "indexing_duration_ms": { + "percentiles": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 0, + "75.0": 0, + "95.0": 0, + "99.0": 0 + } + } + } + }, + { + "timestamp": "2023-05-26T16:00:00.000Z", + "stats": { + "number_of_executions": { + "total": 9, + "by_outcome": { + "succeeded": 9, + "warning": 0, + "failed": 0 + } + }, + "number_of_logged_messages": { + "total": 18, + "by_level": { + "error": 0, + "warn": 0, + "info": 18, + "debug": 0, + "trace": 0 + } + }, + "number_of_detected_gaps": { + "total": 0, + "total_duration_s": 0 + }, + "schedule_delay_ms": { + "percentiles": { + "1.0": 3061, + "5.0": 3061, + "25.0": 3104.75, + "50.0": 3115, + "75.0": 6053, + "95.0": 6068, + "99.0": 6068 + } + }, + "execution_duration_ms": { + "percentiles": { + "1.0": 226.00000000000003, + "5.0": 226, + "25.0": 356, + "50.0": 436, + "75.0": 495.5, + "95.0": 719, + "99.0": 719 + } + }, + "search_duration_ms": { + "percentiles": { + "1.0": 2, + "5.0": 2, + "25.0": 5.75, + "50.0": 8, + "75.0": 13.75, + "95.0": 17, + "99.0": 17 + } + }, + "indexing_duration_ms": { + "percentiles": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 0, + "75.0": 0, + "95.0": 0, + "99.0": 0 + } + } + } + } + ] + } + } +} +``` + +## Space health endpoint + +🚧 NOTE: this endpoint is **partially implemented**. 🚧 + +```txt +POST /internal/detection_engine/health/_space +GET /internal/detection_engine/health/_space +``` + +Get health overview of the current Kibana space. Scope: all detection rules in the space. +Returns: + +- health stats at the moment of the API call +- health stats over a specified period of time ("health interval") +- health stats history within the same interval in the form of a histogram + (the same stats are calculated over each of the discreet sub-intervals of the whole interval) + +Minimal required parameters for the `POST` route: empty object. + +```json +{} +``` + +The `GET` route doesn't accept any parameters and uses the default parameters instead: + +- interval: `last_day` +- granularity: `hour` +- debug: `false` + +Response: + +```json +{ + "timings": { + "requested_at": "2023-05-26T16:24:21.628Z", + "processed_at": "2023-05-26T16:24:22.880Z", + "processing_time_ms": 1252 + }, + "parameters": { + "interval": { + "type": "last_day", + "granularity": "hour", + "from": "2023-05-25T16:24:21.628Z", + "to": "2023-05-26T16:24:21.628Z", + "duration": "PT24H" + } + }, + "health": { + "state_at_the_moment": { + "number_of_rules": { + "all": { + "total": 777, + "enabled": 777, + "disabled": 0 + }, + "by_origin": { + "prebuilt": { + "total": 776, + "enabled": 776, + "disabled": 0 + }, + "custom": { + "total": 1, + "enabled": 1, + "disabled": 0 + } + }, + "by_type": { + "siem.eqlRule": { + "total": 381, + "enabled": 381, + "disabled": 0 + }, + "siem.queryRule": { + "total": 325, + "enabled": 325, + "disabled": 0 + }, + "siem.mlRule": { + "total": 47, + "enabled": 47, + "disabled": 0 + }, + "siem.thresholdRule": { + "total": 18, + "enabled": 18, + "disabled": 0 + }, + "siem.newTermsRule": { + "total": 4, + "enabled": 4, + "disabled": 0 + }, + "siem.indicatorRule": { + "total": 2, + "enabled": 2, + "disabled": 0 + } + }, + "by_outcome": { + "warning": { + "total": 307, + "enabled": 307, + "disabled": 0 + }, + "succeeded": { + "total": 266, + "enabled": 266, + "disabled": 0 + }, + "failed": { + "total": 204, + "enabled": 204, + "disabled": 0 + } + } + } + }, + "stats_over_interval": { + "number_of_executions": { + "total": 5622, + "by_outcome": { + "succeeded": 1882, + "warning": 2129, + "failed": 2120 + } + }, + "number_of_logged_messages": { + "total": 11756, + "by_level": { + "error": 2120, + "warn": 2129, + "info": 7507, + "debug": 0, + "trace": 0 + } + }, + "number_of_detected_gaps": { + "total": 777, + "total_duration_s": 514415894 + }, + "schedule_delay_ms": { + "percentiles": { + "1.0": 216, + "5.0": 3048.5, + "25.0": 3105, + "50.0": 3129, + "75.0": 6112.355119825708, + "95.0": 134006, + "99.0": 195578 + } + }, + "execution_duration_ms": { + "percentiles": { + "1.0": 275, + "5.0": 323.375, + "25.0": 370.80555555555554, + "50.0": 413.1122337092731, + "75.0": 502.25233127864715, + "95.0": 685.8055555555555, + "99.0": 1194.75 + } + }, + "search_duration_ms": { + "percentiles": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 0, + "75.0": 15, + "95.0": 30, + "99.0": 99.44000000000005 + } + }, + "indexing_duration_ms": { + "percentiles": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 0, + "75.0": 0, + "95.0": 0, + "99.0": 0 + } + }, + "top_errors": [ + { + "count": 1202, + "message": "An error occurred during rule execution message verification_exception" + }, + { + "count": 777, + "message": "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" + }, + { + "count": 3, + "message": "An error occurred during rule execution message rare_error_code missing" + }, + { + "count": 3, + "message": "An error occurred during rule execution message v3_windows_anomalous_path_activity missing" + }, + { + "count": 3, + "message": "An error occurred during rule execution message v3_windows_rare_user_type10_remote_login missing" + } + ], + "top_warnings": [ + { + "count": 2129, + "message": "This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching was found This warning will continue to appear until matching index is created or this rule is disabled" + } + ] + }, + "history_over_interval": { + "buckets": [ + { + "timestamp": "2023-05-26T15:00:00.000Z", + "stats": { + "number_of_executions": { + "total": 2245, + "by_outcome": { + "succeeded": 566, + "warning": 849, + "failed": 1336 + } + }, + "number_of_logged_messages": { + "total": 4996, + "by_level": { + "error": 1336, + "warn": 849, + "info": 2811, + "debug": 0, + "trace": 0 + } + }, + "number_of_detected_gaps": { + "total": 777, + "total_duration_s": 514415894 + }, + "schedule_delay_ms": { + "percentiles": { + "1.0": 256, + "5.0": 3086.9722222222217, + "25.0": 3133, + "50.0": 6126, + "75.0": 59484.25, + "95.0": 179817.25, + "99.0": 202613 + } + }, + "execution_duration_ms": { + "percentiles": { + "1.0": 280.6, + "5.0": 327.7, + "25.0": 371.5208333333333, + "50.0": 415.6190476190476, + "75.0": 505.7642857142857, + "95.0": 740.4375, + "99.0": 1446.1500000000005 + } + }, + "search_duration_ms": { + "percentiles": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 0, + "75.0": 8, + "95.0": 25, + "99.0": 46 + } + }, + "indexing_duration_ms": { + "percentiles": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 0, + "75.0": 0, + "95.0": 0, + "99.0": 0 + } + } + } + }, + { + "timestamp": "2023-05-26T16:00:00.000Z", + "stats": { + "number_of_executions": { + "total": 3363, + "by_outcome": { + "succeeded": 1316, + "warning": 1280, + "failed": 784 + } + }, + "number_of_logged_messages": { + "total": 6760, + "by_level": { + "error": 784, + "warn": 1280, + "info": 4696, + "debug": 0, + "trace": 0 + } + }, + "number_of_detected_gaps": { + "total": 0, + "total_duration_s": 0 + }, + "schedule_delay_ms": { + "percentiles": { + "1.0": 207, + "5.0": 3042, + "25.0": 3098.46511627907, + "50.0": 3112, + "75.0": 3145.2820512820517, + "95.0": 6100.571428571428, + "99.0": 6123 + } + }, + "execution_duration_ms": { + "percentiles": { + "1.0": 275, + "5.0": 319.85714285714283, + "25.0": 370.0357142857143, + "50.0": 410.79999229108853, + "75.0": 500.7692307692308, + "95.0": 675, + "99.0": 781.3999999999996 + } + }, + "search_duration_ms": { + "percentiles": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 9, + "75.0": 17.555555555555557, + "95.0": 34, + "99.0": 110.5 + } + }, + "indexing_duration_ms": { + "percentiles": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 0, + "75.0": 0, + "95.0": 0, + "99.0": 0 + } + } + } + } + ] + } + } +} +``` + +## Cluster health endpoint + +🚧 NOTE: this endpoint is **partially implemented**. 🚧 + +```txt +POST /internal/detection_engine/health/_cluster +GET /internal/detection_engine/health/_cluster +``` + +Minimal required parameters for the `POST` route: empty object. + +```json +{} +``` + +The `GET` route doesn't accept any parameters and uses the default parameters instead: + +- interval: `last_day` +- granularity: `hour` +- debug: `false` + +Response: + +```json +{ + "timings": { + "requested_at": "2023-09-15T13:41:44.565Z", + "processed_at": "2023-09-15T13:41:44.648Z", + "processing_time_ms": 83 + }, + "parameters": { + "interval": { + "type": "last_day", + "granularity": "hour", + "from": "2023-09-14T13:41:44.565Z", + "to": "2023-09-15T13:41:44.565Z", + "duration": "PT24H" + } + }, + "health": { + "state_at_the_moment": { + "number_of_rules": { + "all": { + "total": 40, + "enabled": 40, + "disabled": 0 + }, + "by_origin": { + "prebuilt": { + "total": 40, + "enabled": 40, + "disabled": 0 + }, + "custom": { + "total": 0, + "enabled": 0, + "disabled": 0 + } + }, + "by_type": { + "siem.queryRule": { + "total": 28, + "enabled": 28, + "disabled": 0 + }, + "siem.mlRule": { + "total": 10, + "enabled": 10, + "disabled": 0 + }, + "siem.newTermsRule": { + "total": 2, + "enabled": 2, + "disabled": 0 + } + }, + "by_outcome": { + "warning": { + "total": 30, + "enabled": 30, + "disabled": 0 + }, + "failed": { + "total": 10, + "enabled": 10, + "disabled": 0 + } + } + } + }, + "stats_over_interval": { + "number_of_executions": { + "total": 290, + "by_outcome": { + "succeeded": 0, + "warning": 240, + "failed": 90 + } + }, + "number_of_logged_messages": { + "total": 620, + "by_level": { + "error": 90, + "warn": 240, + "info": 290, + "debug": 0, + "trace": 0 + } + }, + "number_of_detected_gaps": { + "total": 40, + "total_duration_s": 12986680 + }, + "schedule_delay_ms": { + "percentiles": { + "50.0": 261, + "95.0": 330999215.3, + "99.0": 331057597, + "99.9": 331057597 + } + }, + "execution_duration_ms": { + "percentiles": { + "50.0": 530.5, + "95.0": 1864.350000000016, + "99.0": 13863.33, + "99.9": 13871.133 + } + }, + "search_duration_ms": { + "percentiles": { + "50.0": 0, + "95.0": 0, + "99.0": 0, + "99.9": 0 + } + }, + "indexing_duration_ms": { + "percentiles": { + "50.0": 0, + "95.0": 0, + "99.0": 0, + "99.9": 0 + } + }, + "top_errors": [ + { + "count": 40, + "message": "days 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" + }, + { + "count": 10, + "message": "An error occurred during rule execution message high_distinct_count_error_message missing" + }, + { + "count": 10, + "message": "An error occurred during rule execution message rare_error_code missing" + }, + { + "count": 10, + "message": "An error occurred during rule execution message rare_method_for_a_city missing" + }, + { + "count": 10, + "message": "An error occurred during rule execution message rare_method_for_a_username missing" + } + ], + "top_warnings": [ + { + "count": 240, + "message": "This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching filebeat logs-aws was found This warning will continue to appear until matching index is created or this rule is disabled" + } + ] + }, + "history_over_interval": { + "buckets": [ + { + "timestamp": "2023-09-15T12:00:00.000Z", + "stats": { + "number_of_executions": { + "total": 110, + "by_outcome": { + "succeeded": 0, + "warning": 90, + "failed": 60 + } + }, + "number_of_logged_messages": { + "total": 260, + "by_level": { + "error": 60, + "warn": 90, + "info": 110, + "debug": 0, + "trace": 0 + } + }, + "number_of_detected_gaps": { + "total": 40, + "total_duration_s": 12986680 + }, + "schedule_delay_ms": { + "percentiles": { + "50.0": 2975, + "95.0": 331046564.55, + "99.0": 331057597, + "99.9": 331057597 + } + }, + "execution_duration_ms": { + "percentiles": { + "50.0": 677.5, + "95.0": 8943.65, + "99.0": 13868.73, + "99.9": 13871.673 + } + }, + "search_duration_ms": { + "percentiles": { + "50.0": 0, + "95.0": 0, + "99.0": 0, + "99.9": 0 + } + }, + "indexing_duration_ms": { + "percentiles": { + "50.0": 0, + "95.0": 0, + "99.0": 0, + "99.9": 0 + } + } + } + }, + { + "timestamp": "2023-09-15T13:00:00.000Z", + "stats": { + "number_of_executions": { + "total": 180, + "by_outcome": { + "succeeded": 0, + "warning": 150, + "failed": 30 + } + }, + "number_of_logged_messages": { + "total": 360, + "by_level": { + "error": 30, + "warn": 150, + "info": 180, + "debug": 0, + "trace": 0 + } + }, + "number_of_detected_gaps": { + "total": 0, + "total_duration_s": 0 + }, + "schedule_delay_ms": { + "percentiles": { + "50.0": 246.5, + "95.0": 3245.35, + "99.0": 3905.0100000000216, + "99.9": 6173.243000000005 + } + }, + "execution_duration_ms": { + "percentiles": { + "50.0": 503.5, + "95.0": 692.15, + "99.0": 758.63, + "99.9": 763.4630000000001 + } + }, + "search_duration_ms": { + "percentiles": { + "50.0": 0, + "95.0": 0, + "99.0": 0, + "99.9": 0 + } + }, + "indexing_duration_ms": { + "percentiles": { + "50.0": 0, + "95.0": 0, + "99.0": 0, + "99.9": 0 + } + } + } + } + ] + } + } +} +``` + +## Optional parameters + +All the three endpoints accept optional `interval` and `debug` request parameters. + +### Health interval + +You can change the interval over which the health stats will be calculated. If you don't specify it, by default health stats will be calculated over the last day with the granularity of 1 hour. + +```json +{ + "interval": { + "type": "last_week", + "granularity": "day" + } +} +``` + +You can also specify a custom date range with exact interval bounds. + +```json +{ + "interval": { + "type": "custom_range", + "granularity": "minute", + "from": "2023-05-20T16:24:21.628Z", + "to": "2023-05-26T16:24:21.628Z" + } +} +``` + +Please keep in mind that requesting large intervals with small granularity can generate substantial load on the system and enormous API responses. + +### Debug mode + +You can also include various debug information in the response, such as queries and aggregations sent to Elasticsearch and response received from it. + +```json +{ + "debug": true +} +``` + +In the response you will find something like that: + +```json +{ + "health": { + "debug": { + "rulesClient": { + "request": { + "aggs": { + "rulesByEnabled": { + "terms": { + "field": "alert.attributes.enabled" + } + }, + "rulesByOrigin": { + "terms": { + "field": "alert.attributes.params.immutable" + }, + "aggs": { + "rulesByEnabled": { + "terms": { + "field": "alert.attributes.enabled" + } + } + } + }, + "rulesByType": { + "terms": { + "field": "alert.attributes.alertTypeId" + }, + "aggs": { + "rulesByEnabled": { + "terms": { + "field": "alert.attributes.enabled" + } + } + } + }, + "rulesByOutcome": { + "terms": { + "field": "alert.attributes.lastRun.outcome" + }, + "aggs": { + "rulesByEnabled": { + "terms": { + "field": "alert.attributes.enabled" + } + } + } + } + } + }, + "response": { + "aggregations": { + "rulesByOutcome": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "warning", + "doc_count": 307, + "rulesByEnabled": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": 1, + "key_as_string": "true", + "doc_count": 307 + } + ] + } + }, + { + "key": "succeeded", + "doc_count": 266, + "rulesByEnabled": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": 1, + "key_as_string": "true", + "doc_count": 266 + } + ] + } + }, + { + "key": "failed", + "doc_count": 204, + "rulesByEnabled": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": 1, + "key_as_string": "true", + "doc_count": 204 + } + ] + } + } + ] + }, + "rulesByType": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "siem.eqlRule", + "doc_count": 381, + "rulesByEnabled": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": 1, + "key_as_string": "true", + "doc_count": 381 + } + ] + } + }, + { + "key": "siem.queryRule", + "doc_count": 325, + "rulesByEnabled": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": 1, + "key_as_string": "true", + "doc_count": 325 + } + ] + } + }, + { + "key": "siem.mlRule", + "doc_count": 47, + "rulesByEnabled": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": 1, + "key_as_string": "true", + "doc_count": 47 + } + ] + } + }, + { + "key": "siem.thresholdRule", + "doc_count": 18, + "rulesByEnabled": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": 1, + "key_as_string": "true", + "doc_count": 18 + } + ] + } + }, + { + "key": "siem.newTermsRule", + "doc_count": 4, + "rulesByEnabled": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": 1, + "key_as_string": "true", + "doc_count": 4 + } + ] + } + }, + { + "key": "siem.indicatorRule", + "doc_count": 2, + "rulesByEnabled": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": 1, + "key_as_string": "true", + "doc_count": 2 + } + ] + } + } + ] + }, + "rulesByOrigin": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "true", + "doc_count": 776, + "rulesByEnabled": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": 1, + "key_as_string": "true", + "doc_count": 776 + } + ] + } + }, + { + "key": "false", + "doc_count": 1, + "rulesByEnabled": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": 1, + "key_as_string": "true", + "doc_count": 1 + } + ] + } + } + ] + }, + "rulesByEnabled": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": 1, + "key_as_string": "true", + "doc_count": 777 + } + ] + } + } + } + }, + "eventLog": { + "request": { + "aggs": { + "totalExecutions": { + "cardinality": { + "field": "kibana.alert.rule.execution.uuid" + } + }, + "executeEvents": { + "filter": { + "term": { + "event.action": "execute" + } + }, + "aggs": { + "executionDurationMs": { + "percentiles": { + "field": "kibana.alert.rule.execution.metrics.total_run_duration_ms", + "missing": 0, + "percents": [1, 5, 25, 50, 75, 95, 99] + } + }, + "scheduleDelayNs": { + "percentiles": { + "field": "kibana.task.schedule_delay", + "missing": 0, + "percents": [1, 5, 25, 50, 75, 95, 99] + } + } + } + }, + "statusChangeEvents": { + "filter": { + "bool": { + "filter": [ + { + "term": { + "event.action": "status-change" + } + } + ], + "must_not": [ + { + "terms": { + "kibana.alert.rule.execution.status": ["running", "going to run"] + } + } + ] + } + }, + "aggs": { + "executionsByStatus": { + "terms": { + "field": "kibana.alert.rule.execution.status" + } + } + } + }, + "executionMetricsEvents": { + "filter": { + "term": { + "event.action": "execution-metrics" + } + }, + "aggs": { + "gaps": { + "filter": { + "exists": { + "field": "kibana.alert.rule.execution.metrics.execution_gap_duration_s" + } + }, + "aggs": { + "totalGapDurationS": { + "sum": { + "field": "kibana.alert.rule.execution.metrics.execution_gap_duration_s" + } + } + } + }, + "searchDurationMs": { + "percentiles": { + "field": "kibana.alert.rule.execution.metrics.total_search_duration_ms", + "missing": 0, + "percents": [1, 5, 25, 50, 75, 95, 99] + } + }, + "indexingDurationMs": { + "percentiles": { + "field": "kibana.alert.rule.execution.metrics.total_indexing_duration_ms", + "missing": 0, + "percents": [1, 5, 25, 50, 75, 95, 99] + } + } + } + }, + "messageContainingEvents": { + "filter": { + "terms": { + "event.action": ["status-change", "message"] + } + }, + "aggs": { + "messagesByLogLevel": { + "terms": { + "field": "log.level" + } + }, + "errors": { + "filter": { + "term": { + "log.level": "error" + } + }, + "aggs": { + "topErrors": { + "categorize_text": { + "field": "message", + "size": 5, + "similarity_threshold": 99 + } + } + } + }, + "warnings": { + "filter": { + "term": { + "log.level": "warn" + } + }, + "aggs": { + "topWarnings": { + "categorize_text": { + "field": "message", + "size": 5, + "similarity_threshold": 99 + } + } + } + } + } + }, + "statsHistory": { + "date_histogram": { + "field": "@timestamp", + "calendar_interval": "hour" + }, + "aggs": { + "totalExecutions": { + "cardinality": { + "field": "kibana.alert.rule.execution.uuid" + } + }, + "executeEvents": { + "filter": { + "term": { + "event.action": "execute" + } + }, + "aggs": { + "executionDurationMs": { + "percentiles": { + "field": "kibana.alert.rule.execution.metrics.total_run_duration_ms", + "missing": 0, + "percents": [1, 5, 25, 50, 75, 95, 99] + } + }, + "scheduleDelayNs": { + "percentiles": { + "field": "kibana.task.schedule_delay", + "missing": 0, + "percents": [1, 5, 25, 50, 75, 95, 99] + } + } + } + }, + "statusChangeEvents": { + "filter": { + "bool": { + "filter": [ + { + "term": { + "event.action": "status-change" + } + } + ], + "must_not": [ + { + "terms": { + "kibana.alert.rule.execution.status": ["running", "going to run"] + } + } + ] + } + }, + "aggs": { + "executionsByStatus": { + "terms": { + "field": "kibana.alert.rule.execution.status" + } + } + } + }, + "executionMetricsEvents": { + "filter": { + "term": { + "event.action": "execution-metrics" + } + }, + "aggs": { + "gaps": { + "filter": { + "exists": { + "field": "kibana.alert.rule.execution.metrics.execution_gap_duration_s" + } + }, + "aggs": { + "totalGapDurationS": { + "sum": { + "field": "kibana.alert.rule.execution.metrics.execution_gap_duration_s" + } + } + } + }, + "searchDurationMs": { + "percentiles": { + "field": "kibana.alert.rule.execution.metrics.total_search_duration_ms", + "missing": 0, + "percents": [1, 5, 25, 50, 75, 95, 99] + } + }, + "indexingDurationMs": { + "percentiles": { + "field": "kibana.alert.rule.execution.metrics.total_indexing_duration_ms", + "missing": 0, + "percents": [1, 5, 25, 50, 75, 95, 99] + } + } + } + }, + "messageContainingEvents": { + "filter": { + "terms": { + "event.action": ["status-change", "message"] + } + }, + "aggs": { + "messagesByLogLevel": { + "terms": { + "field": "log.level" + } + } + } + } + } + } + } + }, + "response": { + "aggregations": { + "statsHistory": { + "buckets": [ + { + "key_as_string": "2023-05-26T15:00:00.000Z", + "key": 1685113200000, + "doc_count": 11388, + "statusChangeEvents": { + "doc_count": 2751, + "executionsByStatus": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "failed", + "doc_count": 1336 + }, + { + "key": "partial failure", + "doc_count": 849 + }, + { + "key": "succeeded", + "doc_count": 566 + } + ] + } + }, + "totalExecutions": { + "value": 2245 + }, + "messageContainingEvents": { + "doc_count": 4996, + "messagesByLogLevel": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "info", + "doc_count": 2811 + }, + { + "key": "error", + "doc_count": 1336 + }, + { + "key": "warn", + "doc_count": 849 + } + ] + } + }, + "executeEvents": { + "doc_count": 2245, + "scheduleDelayNs": { + "values": { + "1.0": 256000000, + "5.0": 3086972222.222222, + "25.0": 3133000000, + "50.0": 6126000000, + "75.0": 59484250000, + "95.0": 179817250000, + "99.0": 202613000000 + } + }, + "executionDurationMs": { + "values": { + "1.0": 280.6, + "5.0": 327.7, + "25.0": 371.5208333333333, + "50.0": 415.6190476190476, + "75.0": 505.575, + "95.0": 740.4375, + "99.0": 1446.1500000000005 + } + } + }, + "executionMetricsEvents": { + "doc_count": 1902, + "searchDurationMs": { + "values": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 0, + "75.0": 8, + "95.0": 25, + "99.0": 46 + } + }, + "gaps": { + "doc_count": 777, + "totalGapDurationS": { + "value": 514415894 + } + }, + "indexingDurationMs": { + "values": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 0, + "75.0": 0, + "95.0": 0, + "99.0": 0 + } + } + } + }, + { + "key_as_string": "2023-05-26T16:00:00.000Z", + "key": 1685116800000, + "doc_count": 28325, + "statusChangeEvents": { + "doc_count": 6126, + "executionsByStatus": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "succeeded", + "doc_count": 2390 + }, + { + "key": "partial failure", + "doc_count": 2305 + }, + { + "key": "failed", + "doc_count": 1431 + } + ] + } + }, + "totalExecutions": { + "value": 6170 + }, + "messageContainingEvents": { + "doc_count": 12252, + "messagesByLogLevel": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "info", + "doc_count": 8516 + }, + { + "key": "warn", + "doc_count": 2305 + }, + { + "key": "error", + "doc_count": 1431 + } + ] + } + }, + "executeEvents": { + "doc_count": 6126, + "scheduleDelayNs": { + "values": { + "1.0": 193000000, + "5.0": 3017785185.1851854, + "25.0": 3086000000, + "50.0": 3105877192.982456, + "75.0": 3134645161.290323, + "95.0": 6081772222.222222, + "99.0": 6122000000 + } + }, + "executionDurationMs": { + "values": { + "1.0": 275.17333333333335, + "5.0": 324.8014285714285, + "25.0": 377.0752688172043, + "50.0": 431, + "75.0": 532.3870967741935, + "95.0": 720.6761904761904, + "99.0": 922.6799999999985 + } + } + }, + "executionMetricsEvents": { + "doc_count": 3821, + "searchDurationMs": { + "values": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 9.8, + "75.0": 18, + "95.0": 40.17499999999999, + "99.0": 124 + } + }, + "gaps": { + "doc_count": 0, + "totalGapDurationS": { + "value": 0 + } + }, + "indexingDurationMs": { + "values": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 0, + "75.0": 0, + "95.0": 0, + "99.0": 0 + } + } + } + } + ] + }, + "statusChangeEvents": { + "doc_count": 8877, + "executionsByStatus": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "partial failure", + "doc_count": 3154 + }, + { + "key": "succeeded", + "doc_count": 2956 + }, + { + "key": "failed", + "doc_count": 2767 + } + ] + } + }, + "totalExecutions": { + "value": 8455 + }, + "messageContainingEvents": { + "doc_count": 17248, + "messagesByLogLevel": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "info", + "doc_count": 11327 + }, + { + "key": "warn", + "doc_count": 3154 + }, + { + "key": "error", + "doc_count": 2767 + } + ] + }, + "warnings": { + "doc_count": 3154, + "topWarnings": { + "buckets": [ + { + "doc_count": 3154, + "key": "This rule is attempting to query data from Elasticsearch indices listed in the Index pattern section of the rule definition however no index matching was found This warning will continue to appear until matching index is created or this rule is disabled", + "regex": ".*?This.+?rule.+?is.+?attempting.+?to.+?query.+?data.+?from.+?Elasticsearch.+?indices.+?listed.+?in.+?the.+?Index.+?pattern.+?section.+?of.+?the.+?rule.+?definition.+?however.+?no.+?index.+?matching.+?was.+?found.+?This.+?warning.+?will.+?continue.+?to.+?appear.+?until.+?matching.+?index.+?is.+?created.+?or.+?this.+?rule.+?is.+?disabled.*?", + "max_matching_length": 342 + } + ] + } + }, + "errors": { + "doc_count": 2767, + "topErrors": { + "buckets": [ + { + "doc_count": 1802, + "key": "An error occurred during rule execution message verification_exception", + "regex": ".*?An.+?error.+?occurred.+?during.+?rule.+?execution.+?message.+?verification_exception.*?", + "max_matching_length": 2064 + }, + { + "doc_count": 777, + "key": "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", + "regex": ".*?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.*?", + "max_matching_length": 216 + }, + { + "doc_count": 4, + "key": "An error occurred during rule execution message rare_error_code missing", + "regex": ".*?An.+?error.+?occurred.+?during.+?rule.+?execution.+?message.+?rare_error_code.+?missing.*?", + "max_matching_length": 82 + }, + { + "doc_count": 4, + "key": "An error occurred during rule execution message v3_windows_anomalous_path_activity missing", + "regex": ".*?An.+?error.+?occurred.+?during.+?rule.+?execution.+?message.+?v3_windows_anomalous_path_activity.+?missing.*?", + "max_matching_length": 103 + }, + { + "doc_count": 4, + "key": "An error occurred during rule execution message v3_windows_rare_user_type10_remote_login missing", + "regex": ".*?An.+?error.+?occurred.+?during.+?rule.+?execution.+?message.+?v3_windows_rare_user_type10_remote_login.+?missing.*?", + "max_matching_length": 110 + } + ] + } + } + }, + "executeEvents": { + "doc_count": 8371, + "scheduleDelayNs": { + "values": { + "1.0": 206000000, + "5.0": 3027000000, + "25.0": 3092000000, + "50.0": 3116000000, + "75.0": 3278666666.6666665, + "95.0": 99656950000, + "99.0": 186632790000 + } + }, + "executionDurationMs": { + "values": { + "1.0": 275.5325, + "5.0": 326.07857142857137, + "25.0": 375.68969144460027, + "50.0": 427, + "75.0": 526.2948717948718, + "95.0": 727.2480952380952, + "99.0": 1009.5299999999934 + } + } + }, + "executionMetricsEvents": { + "doc_count": 5723, + "searchDurationMs": { + "values": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 4, + "75.0": 16, + "95.0": 34.43846153846145, + "99.0": 116.51333333333302 + } + }, + "gaps": { + "doc_count": 777, + "totalGapDurationS": { + "value": 514415894 + } + }, + "indexingDurationMs": { + "values": { + "1.0": 0, + "5.0": 0, + "25.0": 0, + "50.0": 0, + "75.0": 0, + "95.0": 0, + "99.0": 0 + } + } + } + } + } + } + } + } +} +``` diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/cluster_health.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/cluster_health.mock.ts index 6bd740a87cf54..6fac9e9b38521 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/cluster_health.mock.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/cluster_health.mock.ts @@ -10,17 +10,13 @@ import { healthStatsMock } from './health_stats.mock'; const getEmptyClusterHealthSnapshot = (): ClusterHealthSnapshot => { return { - stats_at_the_moment: healthStatsMock.getEmptyRuleStats(), - stats_over_interval: { - message: 'Not implemented', - }, + state_at_the_moment: healthStatsMock.getEmptyHealthOverviewState(), + stats_over_interval: healthStatsMock.getEmptyHealthOverviewStats(), history_over_interval: { buckets: [ { timestamp: '2023-05-15T16:12:14.967Z', - stats: { - message: 'Not implemented', - }, + stats: healthStatsMock.getEmptyHealthOverviewStats(), }, ], }, diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/cluster_health.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/cluster_health.ts index 441eef935ade5..bbf838a828dfe 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/cluster_health.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/cluster_health.ts @@ -6,7 +6,7 @@ */ import type { HealthParameters, HealthSnapshot } from './health_metadata'; -import type { RuleStats, StatsHistory } from './health_stats'; +import type { HealthOverviewStats, HealthOverviewState, HealthHistory } from './health_stats'; /** * Health calculation parameters for the whole cluster. @@ -18,30 +18,27 @@ export type ClusterHealthParameters = HealthParameters; */ export interface ClusterHealthSnapshot extends HealthSnapshot { /** - * Health stats at the moment of the calculation request. + * Health state at the moment of the calculation request. */ - stats_at_the_moment: ClusterHealthStatsAtTheMoment; + state_at_the_moment: ClusterHealthState; /** * Health stats calculated over the interval specified in the health parameters. */ - stats_over_interval: ClusterHealthStatsOverInterval; + stats_over_interval: ClusterHealthStats; /** * History of change of the same health stats during the interval. */ - history_over_interval: StatsHistory; + history_over_interval: HealthHistory; } /** - * Health stats at the moment of the calculation request. + * Health state at the moment of the calculation request. */ -export type ClusterHealthStatsAtTheMoment = RuleStats; +export type ClusterHealthState = HealthOverviewState; /** * Health stats calculated over a given interval. */ -export interface ClusterHealthStatsOverInterval { - // TODO: https://github.com/elastic/kibana/issues/125642 Implement and delete this `message` - message: 'Not implemented'; -} +export type ClusterHealthStats = HealthOverviewStats; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/health_stats.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/health_stats.mock.ts index 545a1b0ef0440..ffb9275bdd89a 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/health_stats.mock.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/health_stats.mock.ts @@ -7,12 +7,12 @@ import type { AggregatedMetric, - RuleExecutionStats, - RuleStats, + HealthOverviewStats, + HealthOverviewState, TotalEnabledDisabled, } from './health_stats'; -const getEmptyRuleStats = (): RuleStats => { +const getEmptyHealthOverviewState = (): HealthOverviewState => { return { number_of_rules: { all: getZeroTotalEnabledDisabled(), @@ -34,7 +34,7 @@ const getZeroTotalEnabledDisabled = (): TotalEnabledDisabled => { }; }; -const getEmptyRuleExecutionStats = (): RuleExecutionStats => { +const getEmptyHealthOverviewStats = (): HealthOverviewStats => { return { number_of_executions: { total: 0, @@ -70,18 +70,15 @@ const getEmptyRuleExecutionStats = (): RuleExecutionStats => { const getZeroAggregatedMetric = (): AggregatedMetric => { return { percentiles: { - '1.0': 0, - '5.0': 0, - '25.0': 0, '50.0': 0, - '75.0': 0, '95.0': 0, '99.0': 0, + '99.9': 0, }, }; }; export const healthStatsMock = { - getEmptyRuleStats, - getEmptyRuleExecutionStats, + getEmptyHealthOverviewState, + getEmptyHealthOverviewStats, }; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/health_stats.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/health_stats.ts index 4af7ea8c6bd07..3098b3c69c4a3 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/health_stats.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/health_stats.ts @@ -10,7 +10,7 @@ import type { RuleLastRunOutcomes } from '@kbn/alerting-plugin/common'; import type { LogLevel } from '../../model'; // ------------------------------------------------------------------------------------------------- -// Stats history (date histogram) +// History of health stats (date histogram) /** * History of change of a set of stats over a time interval. The interval is split into discreet buckets, @@ -19,14 +19,14 @@ import type { LogLevel } from '../../model'; * This model corresponds to the `date_histogram` aggregation of Elasticsearch: * https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html */ -export interface StatsHistory { - buckets: Array>; +export interface HealthHistory { + buckets: Array>; } /** * Sub-interval with stats calculated over it. */ -export interface StatsBucket { +export interface HealthBucket { /** * Start timestamp of the sub-interval. */ @@ -39,23 +39,15 @@ export interface StatsBucket { } // ------------------------------------------------------------------------------------------------- -// Rule stats - -// TODO: https://github.com/elastic/kibana/issues/125642 Add more stats, such as: -// - number of Kibana instances -// - number of Kibana spaces -// - number of rules with exceptions -// - number of rules with notification actions (total, normal, legacy) -// - number of rules with response actions -// - top X last failed status messages + rule ids for each status -// - top X last partial failure status messages + rule ids for each status -// - top X slowest rules by any metrics (last total execution time, search time, indexing time, etc) -// - top X rules with the largest schedule delay (drift) +// Health overview state + +// TODO: https://github.com/elastic/kibana/issues/125642 Add more data, see health_data.md /** - * "Static" stats calculated for a set of rules, such as number of enabled and disabled rules, etc. + * "Static" health state at the moment of the API call. Calculated for a set of rules. + * Example: number of enabled and disabled rules. */ -export interface RuleStats { +export interface HealthOverviewState { /** * Various counts of different rules. */ @@ -108,19 +100,15 @@ export interface TotalEnabledDisabled { } // ------------------------------------------------------------------------------------------------- -// Rule execution stats +// Health overview stats -// TODO: https://github.com/elastic/kibana/issues/125642 Add more stats, such as: -// - number of detected alerts (source event "hits") -// - number of created alerts (those we wrote to the .alerts-* index) -// - number of times rule hit cirquit breaker, number of not created alerts because of that -// - number of triggered actions -// - top gaps +// TODO: https://github.com/elastic/kibana/issues/125642 Add more data, see health_data.md /** - * "Dynamic" rule execution stats. Can be calculated either for a set of rules or for a single rule. + * "Dynamic" health stats over a specified "health interval". Can be calculated either + * for a set of rules or for a single rule. */ -export interface RuleExecutionStats { +export interface HealthOverviewStats { /** * Number of rule executions. */ @@ -242,13 +230,10 @@ export interface AggregatedMetric { * Distribution of values of an aggregated metric represented by a set of discreet percentiles. * @example * { - * '1.0': 27, - * '5.0': 150, - * '25.0': 240, * '50.0': 420, - * '75.0': 700, * '95.0': 2500, * '99.0': 7800, + * '99.9': 10000, * } */ export type Percentiles = Record; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/rule_health.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/rule_health.mock.ts index 961a057b2603e..50cb91cf202c5 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/rule_health.mock.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/rule_health.mock.ts @@ -11,15 +11,15 @@ import type { RuleHealthSnapshot } from './rule_health'; const getEmptyRuleHealthSnapshot = (): RuleHealthSnapshot => { return { - stats_at_the_moment: { + state_at_the_moment: { rule: getRulesSchemaMock(), }, - stats_over_interval: healthStatsMock.getEmptyRuleExecutionStats(), + stats_over_interval: healthStatsMock.getEmptyHealthOverviewStats(), history_over_interval: { buckets: [ { timestamp: '2023-05-15T16:12:14.967Z', - stats: healthStatsMock.getEmptyRuleExecutionStats(), + stats: healthStatsMock.getEmptyHealthOverviewStats(), }, ], }, diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/rule_health.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/rule_health.ts index 59756df926e27..77471270695bb 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/rule_health.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/rule_health.ts @@ -7,7 +7,7 @@ import type { RuleResponse } from '../../../model'; import type { HealthParameters, HealthSnapshot } from './health_metadata'; -import type { RuleExecutionStats, StatsHistory } from './health_stats'; +import type { HealthOverviewStats, HealthHistory } from './health_stats'; /** * Health calculation parameters for a given rule. @@ -24,25 +24,25 @@ export interface RuleHealthParameters extends HealthParameters { */ export interface RuleHealthSnapshot extends HealthSnapshot { /** - * Health stats at the moment of the calculation request. + * Health state at the moment of the calculation request. */ - stats_at_the_moment: RuleHealthStatsAtTheMoment; + state_at_the_moment: RuleHealthState; /** * Health stats calculated over the interval specified in the health parameters. */ - stats_over_interval: RuleHealthStatsOverInterval; + stats_over_interval: RuleHealthStats; /** * History of change of the same health stats during the interval. */ - history_over_interval: StatsHistory; + history_over_interval: HealthHistory; } /** - * Health stats at the moment of the calculation request. + * Health state at the moment of the calculation request. */ -export interface RuleHealthStatsAtTheMoment { +export interface RuleHealthState { /** * Rule object including its current execution summary. */ @@ -52,4 +52,4 @@ export interface RuleHealthStatsAtTheMoment { /** * Health stats calculated over a given interval. */ -export type RuleHealthStatsOverInterval = RuleExecutionStats; +export type RuleHealthStats = HealthOverviewStats; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/space_health.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/space_health.mock.ts index 60e1514cee59e..e445a275d6a8c 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/space_health.mock.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/space_health.mock.ts @@ -10,13 +10,13 @@ import type { SpaceHealthSnapshot } from './space_health'; const getEmptySpaceHealthSnapshot = (): SpaceHealthSnapshot => { return { - stats_at_the_moment: healthStatsMock.getEmptyRuleStats(), - stats_over_interval: healthStatsMock.getEmptyRuleExecutionStats(), + state_at_the_moment: healthStatsMock.getEmptyHealthOverviewState(), + stats_over_interval: healthStatsMock.getEmptyHealthOverviewStats(), history_over_interval: { buckets: [ { timestamp: '2023-05-15T16:12:14.967Z', - stats: healthStatsMock.getEmptyRuleExecutionStats(), + stats: healthStatsMock.getEmptyHealthOverviewStats(), }, ], }, diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/space_health.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/space_health.ts index 35648a9257570..173f8e9af1a62 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/space_health.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring/detection_engine_health/model/space_health.ts @@ -6,7 +6,7 @@ */ import type { HealthParameters, HealthSnapshot } from './health_metadata'; -import type { RuleExecutionStats, RuleStats, StatsHistory } from './health_stats'; +import type { HealthOverviewStats, HealthOverviewState, HealthHistory } from './health_stats'; /** * Health calculation parameters for the current Kibana space. @@ -18,27 +18,27 @@ export type SpaceHealthParameters = HealthParameters; */ export interface SpaceHealthSnapshot extends HealthSnapshot { /** - * Health stats at the moment of the calculation request. + * Health state at the moment of the calculation request. */ - stats_at_the_moment: SpaceHealthStatsAtTheMoment; + state_at_the_moment: SpaceHealthState; /** * Health stats calculated over the interval specified in the health parameters. */ - stats_over_interval: SpaceHealthStatsOverInterval; + stats_over_interval: SpaceHealthStats; /** * History of change of the same health stats during the interval. */ - history_over_interval: StatsHistory; + history_over_interval: HealthHistory; } /** - * Health stats at the moment of the calculation request. + * Health state at the moment of the calculation request. */ -export type SpaceHealthStatsAtTheMoment = RuleStats; +export type SpaceHealthState = HealthOverviewState; /** * Health stats calculated over a given interval. */ -export type SpaceHealthStatsOverInterval = RuleExecutionStats; +export type SpaceHealthStats = HealthOverviewStats; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_cluster_health/get_cluster_health_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_cluster_health/get_cluster_health_route.ts index 6d9faeaa95c7f..719f46788a524 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_cluster_health/get_cluster_health_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_cluster_health/get_cluster_health_route.ts @@ -26,7 +26,7 @@ import { validateGetClusterHealthRequest } from './get_cluster_health_request'; /** * Get health overview of the whole cluster. Scope: all detection rules in all Kibana spaces. * Returns: - * - health stats at the moment of the API call + * - health state at the moment of the API call * - health stats over a specified period of time ("health interval") * - health stats history within the same interval in the form of a histogram * (the same stats are calculated over each of the discreet sub-intervals of the whole interval) @@ -111,8 +111,6 @@ const handleClusterHealthRequest = async (args: HandleClusterHealthRequestArgs) const clusterHealth = await healthClient.calculateClusterHealth(clusterHealthParameters); const responseBody: GetClusterHealthResponse = { - // TODO: https://github.com/elastic/kibana/issues/125642 Implement the endpoint and remove the `message` property - message: 'Not implemented', timings: calculateHealthTimings(params.requestReceivedAt), parameters: clusterHealthParameters, health: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_rule_health/get_rule_health_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_rule_health/get_rule_health_route.ts index 310514d17f968..a69f7961b19f8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_rule_health/get_rule_health_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_rule_health/get_rule_health_route.ts @@ -23,7 +23,7 @@ import { validateGetRuleHealthRequest } from './get_rule_health_request'; /** * Get health overview of a rule. Scope: a given detection rule in the current Kibana space. * Returns: - * - health stats at the moment of the API call (rule and its execution summary) + * - health state at the moment of the API call (rule and its execution summary) * - health stats over a specified period of time ("health interval") * - health stats history within the same interval in the form of a histogram * (the same stats are calculated over each of the discreet sub-intervals of the whole interval) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_space_health/get_space_health_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_space_health/get_space_health_route.ts index d76a2212f29c0..96ced4e34151d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_space_health/get_space_health_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_space_health/get_space_health_route.ts @@ -26,7 +26,7 @@ import { validateGetSpaceHealthRequest } from './get_space_health_request'; /** * Get health overview of the current Kibana space. Scope: all detection rules in the space. * Returns: - * - health stats at the moment of the API call + * - health state at the moment of the API call * - health stats over a specified period of time ("health interval") * - health stats history within the same interval in the form of a histogram * (the same stats are calculated over each of the discreet sub-intervals of the whole interval) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/detection_engine_health_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/detection_engine_health_client.ts index af7066dfa780c..71eb37b7286c6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/detection_engine_health_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/detection_engine_health_client.ts @@ -20,16 +20,19 @@ import type { import type { IEventLogHealthClient } from './event_log/event_log_health_client'; import type { IRuleObjectsHealthClient } from './rule_objects/rule_objects_health_client'; +import type { IRuleSpacesClient } from './rule_spaces/rule_spaces_client'; import type { IDetectionEngineHealthClient } from './detection_engine_health_client_interface'; import { installAssetsForRuleMonitoring } from './assets/install_assets_for_rule_monitoring'; export const createDetectionEngineHealthClient = ( + ruleSpacesClient: IRuleSpacesClient, ruleObjectsHealthClient: IRuleObjectsHealthClient, eventLogHealthClient: IEventLogHealthClient, savedObjectsImporter: ISavedObjectsImporter, - logger: Logger, - currentSpaceId: string + logger: Logger ): IDetectionEngineHealthClient => { + const currentSpaceId = ruleSpacesClient.getCurrentSpaceId(); + return { calculateRuleHealth: (args: RuleHealthParameters): Promise => { return withSecuritySpan('IDetectionEngineHealthClient.calculateRuleHealth', async () => { @@ -41,7 +44,7 @@ export const createDetectionEngineHealthClient = ( const statsBasedOnEventLog = await eventLogHealthClient.calculateRuleHealth(args); return { - stats_at_the_moment: statsBasedOnRuleObjects.stats_at_the_moment, + state_at_the_moment: statsBasedOnRuleObjects.state_at_the_moment, stats_over_interval: statsBasedOnEventLog.stats_over_interval, history_over_interval: statsBasedOnEventLog.history_over_interval, debug: { @@ -73,7 +76,7 @@ export const createDetectionEngineHealthClient = ( ]); return { - stats_at_the_moment: statsBasedOnRuleObjects.stats_at_the_moment, + state_at_the_moment: statsBasedOnRuleObjects.state_at_the_moment, stats_over_interval: statsBasedOnEventLog.stats_over_interval, history_over_interval: statsBasedOnEventLog.history_over_interval, debug: { @@ -104,7 +107,7 @@ export const createDetectionEngineHealthClient = ( ]); return { - stats_at_the_moment: statsBasedOnRuleObjects.stats_at_the_moment, + state_at_the_moment: statsBasedOnRuleObjects.state_at_the_moment, stats_over_interval: statsBasedOnEventLog.stats_over_interval, history_over_interval: statsBasedOnEventLog.history_over_interval, debug: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/event_log/aggregations/health_stats_for_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/event_log/aggregations/health_stats_for_rule.ts index 7704bdd3f2441..72d93f0d67777 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/event_log/aggregations/health_stats_for_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/event_log/aggregations/health_stats_for_rule.ts @@ -11,8 +11,8 @@ import type { AggregateEventsBySavedObjectResult } from '@kbn/event-log-plugin/s import type { HealthIntervalGranularity, RuleHealthSnapshot, - RuleHealthStatsOverInterval, - StatsHistory, + RuleHealthStats, + HealthHistory, } from '../../../../../../../../common/api/detection_engine/rule_monitoring'; import type { RawData } from '../../../utils/normalization'; @@ -57,7 +57,7 @@ const getRuleExecutionStatsHistoryAggregation = ( export const normalizeRuleHealthAggregationResult = ( result: AggregateEventsBySavedObjectResult, requestAggs: Record -): Omit => { +): Omit => { const aggregations = result.aggregations ?? {}; return { stats_over_interval: normalizeRuleExecutionStatsAggregationResult( @@ -76,7 +76,7 @@ export const normalizeRuleHealthAggregationResult = ( const normalizeHistoryOverInterval = ( aggregations: Record -): StatsHistory => { +): HealthHistory => { const statsHistory = aggregations.statsHistory || {}; return { 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 7c3b595be1565..4151355419586 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 @@ -13,7 +13,7 @@ import type { NumberOfDetectedGaps, NumberOfExecutions, NumberOfLoggedMessages, - RuleExecutionStats, + HealthOverviewStats, TopMessages, RuleExecutionStatus, } from '../../../../../../../../common/api/detection_engine/rule_monitoring'; @@ -26,23 +26,33 @@ import { import { DEFAULT_PERCENTILES } from '../../../utils/es_aggregations'; import type { RawData } from '../../../utils/normalization'; import * as f from '../../../event_log/event_log_fields'; +import { + ALERTING_PROVIDER, + RULE_EXECUTION_LOG_PROVIDER, +} from '../../../event_log/event_log_constants'; export type RuleExecutionStatsAggregationLevel = 'whole-interval' | 'histogram'; export const getRuleExecutionStatsAggregation = ( - aggregationContext: RuleExecutionStatsAggregationLevel + aggregationLevel: RuleExecutionStatsAggregationLevel ): Record => { return { - totalExecutions: { - cardinality: { - field: f.RULE_EXECUTION_UUID, - }, - }, executeEvents: { filter: { - term: { [f.EVENT_ACTION]: 'execute' }, + bool: { + filter: [ + { term: { [f.EVENT_PROVIDER]: ALERTING_PROVIDER } }, + { term: { [f.EVENT_ACTION]: 'execute' } }, + { term: { [f.EVENT_CATEGORY]: 'siem' } }, + ], + }, }, aggs: { + totalExecutions: { + cardinality: { + field: f.RULE_EXECUTION_UUID, + }, + }, executionDurationMs: { percentiles: { field: f.RULE_EXECUTION_TOTAL_DURATION_MS, @@ -63,11 +73,8 @@ export const getRuleExecutionStatsAggregation = ( filter: { bool: { filter: [ - { - term: { - [f.EVENT_ACTION]: RuleExecutionEventType['status-change'], - }, - }, + { term: { [f.EVENT_PROVIDER]: RULE_EXECUTION_LOG_PROVIDER } }, + { term: { [f.EVENT_ACTION]: RuleExecutionEventType['status-change'] } }, ], must_not: [ { @@ -91,7 +98,12 @@ export const getRuleExecutionStatsAggregation = ( }, executionMetricsEvents: { filter: { - term: { [f.EVENT_ACTION]: RuleExecutionEventType['execution-metrics'] }, + bool: { + filter: [ + { term: { [f.EVENT_PROVIDER]: RULE_EXECUTION_LOG_PROVIDER } }, + { term: { [f.EVENT_ACTION]: RuleExecutionEventType['execution-metrics'] } }, + ], + }, }, aggs: { gaps: { @@ -126,10 +138,17 @@ export const getRuleExecutionStatsAggregation = ( }, messageContainingEvents: { filter: { - terms: { - [f.EVENT_ACTION]: [ - RuleExecutionEventType['status-change'], - RuleExecutionEventType.message, + bool: { + filter: [ + { term: { [f.EVENT_PROVIDER]: RULE_EXECUTION_LOG_PROVIDER } }, + { + terms: { + [f.EVENT_ACTION]: [ + RuleExecutionEventType['status-change'], + RuleExecutionEventType.message, + ], + }, + }, ], }, }, @@ -139,7 +158,7 @@ export const getRuleExecutionStatsAggregation = ( field: f.LOG_LEVEL, }, }, - ...(aggregationContext === 'whole-interval' + ...(aggregationLevel === 'whole-interval' ? { errors: { filter: { @@ -179,13 +198,13 @@ export const getRuleExecutionStatsAggregation = ( export const normalizeRuleExecutionStatsAggregationResult = ( aggregations: Record, aggregationLevel: RuleExecutionStatsAggregationLevel -): RuleExecutionStats => { - const totalExecutions = aggregations.totalExecutions || {}; +): HealthOverviewStats => { const executeEvents = aggregations.executeEvents || {}; const statusChangeEvents = aggregations.statusChangeEvents || {}; const executionMetricsEvents = aggregations.executionMetricsEvents || {}; const messageContainingEvents = aggregations.messageContainingEvents || {}; + const totalExecutions = executeEvents.totalExecutions || {}; const executionDurationMs = executeEvents.executionDurationMs || {}; const scheduleDelayNs = executeEvents.scheduleDelayNs || {}; const executionsByStatus = statusChangeEvents.executionsByStatus || {}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/event_log/event_log_health_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/event_log/event_log_health_client.ts index f4a164629a618..a5138885b69f5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/event_log/event_log_health_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/event_log/event_log_health_client.ts @@ -5,11 +5,15 @@ * 2.0. */ +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { Logger } from '@kbn/core/server'; +import { SavedObjectsUtils } from '@kbn/core/server'; import type { KueryNode } from '@kbn/es-query'; import type { IEventLogClient } from '@kbn/event-log-plugin/server'; import type { ClusterHealthParameters, ClusterHealthSnapshot, + HealthInterval, RuleHealthParameters, RuleHealthSnapshot, SpaceHealthParameters, @@ -23,6 +27,7 @@ import { RULE_SAVED_OBJECT_TYPE, } from '../../event_log/event_log_constants'; import { kqlOr } from '../../utils/kql'; +import type { IRuleSpacesClient } from '../rule_spaces/rule_spaces_client'; import { getRuleHealthAggregation, normalizeRuleHealthAggregationResult, @@ -32,78 +37,110 @@ import { * Client for calculating health stats based on events in .kibana-event-log-* index. */ export interface IEventLogHealthClient { + /** + * Returns health stats for a given rule in the current Kibana space. + * Calculates the stats based on events in .kibana-event-log-* index. + */ calculateRuleHealth(args: RuleHealthParameters): Promise; + + /** + * Returns health stats for all rules in the current Kibana space. + * Calculates the stats based on events in .kibana-event-log-* index. + */ calculateSpaceHealth(args: SpaceHealthParameters): Promise; + + /** + * Returns health stats for all rules in all existing Kibana spaces (the whole cluster). + * Calculates the stats based on events in .kibana-event-log-* index. + */ calculateClusterHealth(args: ClusterHealthParameters): Promise; } -type RuleHealth = Omit; -type SpaceHealth = Omit; -type ClusterHealth = Omit; +type RuleHealth = Omit; +type SpaceHealth = Omit; +type ClusterHealth = Omit; -export const createEventLogHealthClient = (eventLog: IEventLogClient): IEventLogHealthClient => { - return { - async calculateRuleHealth(args: RuleHealthParameters): Promise { - const { rule_id: ruleId, interval } = args; - const soType = RULE_SAVED_OBJECT_TYPE; - const soIds = [ruleId]; - const eventProviders = [RULE_EXECUTION_LOG_PROVIDER, ALERTING_PROVIDER]; +export const createEventLogHealthClient = ( + eventLog: IEventLogClient, + ruleSpacesClient: IRuleSpacesClient, + logger: Logger +): IEventLogHealthClient => { + const EVENT_PROVIDERS = [RULE_EXECUTION_LOG_PROVIDER, ALERTING_PROVIDER]; + const EVENT_PROVIDERS_FILTER = `${f.EVENT_PROVIDER}: (${kqlOr(EVENT_PROVIDERS)})`; - const kqlFilter = `${f.EVENT_PROVIDER}:${kqlOr(eventProviders)}`; - const aggs = getRuleHealthAggregation(interval.granularity); + async function aggregateEventsForRules( + ruleIds: string[], + interval: HealthInterval, + aggs: Record + ) { + const soType = RULE_SAVED_OBJECT_TYPE; + const soIds = ruleIds; - const result = await eventLog.aggregateEventsBySavedObjectIds(soType, soIds, { + const result = await eventLog.aggregateEventsBySavedObjectIds(soType, soIds, { + start: interval.from, + end: interval.to, + filter: EVENT_PROVIDERS_FILTER, + aggs, + }); + + return result; + } + + async function aggregateEventsForSpaces( + spaceIds: string[], + interval: HealthInterval, + aggs: Record + ) { + const soType = RULE_SAVED_OBJECT_TYPE; + const authFilter = {} as KueryNode; + + // The `aggregateEventsWithAuthFilter` method accepts "namespace ids" instead of "space ids". + // If you have two Kibana spaces with ids ['default', 'space-x'], + // it will only work properly if you pass [undefined, 'space-x']. + const namespaces = spaceIds.map((spaceId) => SavedObjectsUtils.namespaceStringToId(spaceId)); + + const result = await eventLog.aggregateEventsWithAuthFilter( + soType, + authFilter, + { start: interval.from, end: interval.to, - filter: kqlFilter, + filter: EVENT_PROVIDERS_FILTER, aggs, - }); + }, + namespaces + ); + + return result; + } + + return { + async calculateRuleHealth(args: RuleHealthParameters): Promise { + const { rule_id: ruleId, interval } = args; + const aggs = getRuleHealthAggregation(interval.granularity); + const result = await aggregateEventsForRules([ruleId], interval, aggs); return normalizeRuleHealthAggregationResult(result, aggs); }, async calculateSpaceHealth(args: SpaceHealthParameters): Promise { const { interval } = args; - const soType = RULE_SAVED_OBJECT_TYPE; - const authFilter = {} as KueryNode; - const namespaces = undefined; // means current Kibana space - const eventProviders = [RULE_EXECUTION_LOG_PROVIDER, ALERTING_PROVIDER]; - - const kqlFilter = `${f.EVENT_PROVIDER}:${kqlOr(eventProviders)}`; - const aggs = getRuleHealthAggregation(interval.granularity); - // TODO: https://github.com/elastic/kibana/issues/125642 Check with ResponseOps that this is correct usage of this method - const result = await eventLog.aggregateEventsWithAuthFilter( - soType, - authFilter, - { - start: interval.from, - end: interval.to, - filter: kqlFilter, - aggs, - }, - namespaces - ); + const spaceIds = [ruleSpacesClient.getCurrentSpaceId()]; + const aggs = getRuleHealthAggregation(interval.granularity); + const result = await aggregateEventsForSpaces(spaceIds, interval, aggs); return normalizeRuleHealthAggregationResult(result, aggs); }, async calculateClusterHealth(args: ClusterHealthParameters): Promise { - // TODO: https://github.com/elastic/kibana/issues/125642 Implement - return { - stats_over_interval: { - message: 'Not implemented', - }, - history_over_interval: { - buckets: [], - }, - debug: { - eventLog: { - request: {}, - response: {}, - }, - }, - }; + const { interval } = args; + + const spaceIds = await ruleSpacesClient.getAllSpaceIds(); + + const aggs = getRuleHealthAggregation(interval.granularity); + const result = await aggregateEventsForSpaces(spaceIds, interval, aggs); + return normalizeRuleHealthAggregationResult(result, aggs); }, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/aggregations/health_stats_for_cluster.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/aggregations/health_stats_for_cluster.ts new file mode 100644 index 0000000000000..f8596ddb7711c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/aggregations/health_stats_for_cluster.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { ClusterHealthState } from '../../../../../../../../common/api/detection_engine/rule_monitoring'; +import { getRuleStatsAggregation, normalizeRuleStatsAggregation } from './rule_stats'; + +export const getClusterHealthAggregation = (): Record< + string, + estypes.AggregationsAggregationContainer +> => { + return getRuleStatsAggregation(); +}; + +export const normalizeClusterHealthAggregationResult = ( + aggregations: Record | undefined +): ClusterHealthState => { + return normalizeRuleStatsAggregation(aggregations ?? {}); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/aggregations/health_stats_for_space.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/aggregations/health_stats_for_space.ts index aba4e6153ad34..1654459e2ea85 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/aggregations/health_stats_for_space.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/aggregations/health_stats_for_space.ts @@ -6,7 +6,7 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { SpaceHealthStatsAtTheMoment } from '../../../../../../../../common/api/detection_engine/rule_monitoring'; +import type { SpaceHealthState } from '../../../../../../../../common/api/detection_engine/rule_monitoring'; import { getRuleStatsAggregation, normalizeRuleStatsAggregation } from './rule_stats'; export const getSpaceHealthAggregation = (): Record< @@ -18,6 +18,6 @@ export const getSpaceHealthAggregation = (): Record< export const normalizeSpaceHealthAggregationResult = ( aggregations: Record -): SpaceHealthStatsAtTheMoment => { +): SpaceHealthState => { return normalizeRuleStatsAggregation(aggregations); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/aggregations/rule_stats.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/aggregations/rule_stats.ts index 35b75fdbd4e7e..1dc2230909bc3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/aggregations/rule_stats.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/aggregations/rule_stats.ts @@ -7,7 +7,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { - RuleStats, + HealthOverviewState, TotalEnabledDisabled, } from '../../../../../../../../common/api/detection_engine/rule_monitoring'; import type { RawData } from '../../../utils/normalization'; @@ -51,7 +51,9 @@ export const getRuleStatsAggregation = (): Record< }; }; -export const normalizeRuleStatsAggregation = (aggregations: Record): RuleStats => { +export const normalizeRuleStatsAggregation = ( + aggregations: Record +): HealthOverviewState => { const rulesByEnabled = aggregations.rulesByEnabled || {}; const rulesByOrigin = aggregations.rulesByOrigin || {}; const rulesByType = aggregations.rulesByType || {}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/filters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/filters.ts new file mode 100644 index 0000000000000..7acb6e0c5f4c7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/filters.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const DETECTION_RULES_FILTER = 'alert.attributes.consumer: "siem"'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/rule_objects_health_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/rule_objects_health_client.ts index 0ba0ccdeaf529..a3d7cb2ed4061 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/rule_objects_health_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_objects/rule_objects_health_client.ts @@ -6,6 +6,7 @@ */ import type { RulesClientApi } from '@kbn/alerting-plugin/server/types'; +import type { SavedObjectsClientContract, Logger } from '@kbn/core/server'; import type { ClusterHealthParameters, ClusterHealthSnapshot, @@ -14,6 +15,12 @@ import type { SpaceHealthParameters, SpaceHealthSnapshot, } from '../../../../../../../common/api/detection_engine/rule_monitoring'; +import { RULE_SAVED_OBJECT_TYPE } from '../../event_log/event_log_constants'; +import { DETECTION_RULES_FILTER } from './filters'; +import { + getClusterHealthAggregation, + normalizeClusterHealthAggregationResult, +} from './aggregations/health_stats_for_cluster'; import { getSpaceHealthAggregation, normalizeSpaceHealthAggregationResult, @@ -21,36 +28,57 @@ import { import { fetchRuleById } from './fetch_rule_by_id'; /** - * Client for calculating health stats based on rule saved objects. + * Client for calculating health stats based on rule objects (saved objects of type "alert"). */ export interface IRuleObjectsHealthClient { + /** + * Returns health stats for a given rule in the current Kibana space. + * Calculates the stats based on rule objects. + */ calculateRuleHealth(args: RuleHealthParameters): Promise; + + /** + * Returns health stats for all rules in the current Kibana space. + * Calculates the stats based on rule objects. + */ calculateSpaceHealth(args: SpaceHealthParameters): Promise; + + /** + * Returns health stats for all rules in all existing Kibana spaces (the whole cluster). + * Calculates the stats based on rule objects. + */ calculateClusterHealth(args: ClusterHealthParameters): Promise; } -type RuleHealth = Pick; -type SpaceHealth = Pick; -type ClusterHealth = Pick; +type RuleHealth = Pick; +type SpaceHealth = Pick; +type ClusterHealth = Pick; export const createRuleObjectsHealthClient = ( - rulesClient: RulesClientApi + rulesClient: RulesClientApi, + internalSavedObjectsClient: SavedObjectsClientContract, + logger: Logger ): IRuleObjectsHealthClient => { return { async calculateRuleHealth(args: RuleHealthParameters): Promise { const rule = await fetchRuleById(rulesClient, args.rule_id); return { - stats_at_the_moment: { rule }, + state_at_the_moment: { rule }, debug: {}, }; }, async calculateSpaceHealth(args: SpaceHealthParameters): Promise { const aggs = getSpaceHealthAggregation(); - const aggregations = await rulesClient.aggregate({ aggs }); + const aggregations = await rulesClient.aggregate({ + options: { + filter: DETECTION_RULES_FILTER, // make sure to query only detection rules + }, + aggs, + }); return { - stats_at_the_moment: normalizeSpaceHealthAggregationResult(aggregations), + state_at_the_moment: normalizeSpaceHealthAggregationResult(aggregations), debug: { rulesClient: { request: { aggs }, @@ -61,35 +89,21 @@ export const createRuleObjectsHealthClient = ( }, async calculateClusterHealth(args: ClusterHealthParameters): Promise { - // TODO: https://github.com/elastic/kibana/issues/125642 Implement + const aggs = getClusterHealthAggregation(); + const response = await internalSavedObjectsClient.find>({ + type: RULE_SAVED_OBJECT_TYPE, // query rules + filter: DETECTION_RULES_FILTER, // make sure to query only detection rules + namespaces: ['*'], // aggregate rules in all Kibana spaces + perPage: 0, // don't return rules in the response, we only need aggs + aggs, + }); + return { - stats_at_the_moment: { - number_of_rules: { - all: { - total: 0, - enabled: 0, - disabled: 0, - }, - by_origin: { - prebuilt: { - total: 0, - enabled: 0, - disabled: 0, - }, - custom: { - total: 0, - enabled: 0, - disabled: 0, - }, - }, - by_type: {}, - by_outcome: {}, - }, - }, + state_at_the_moment: normalizeClusterHealthAggregationResult(response.aggregations), debug: { - rulesClient: { - request: {}, - response: {}, + savedObjectsClient: { + request: { aggs }, + response, }, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_spaces/aggregations/spaces.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_spaces/aggregations/spaces.ts new file mode 100644 index 0000000000000..bae44b17c2f92 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_spaces/aggregations/spaces.ts @@ -0,0 +1,48 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { RawData } from '../../../utils/normalization'; + +// The number of Kibana spaces is limited by the `maxSpaces` config setting of the spaces plugin. +// At the time of writing this comment, `maxSpaces` defaults to 1000. +// So in normal conditions there can't exist more than 1000 Kibana spaces. +// +// However, we set `MAX_KIBANA_SPACES` to a higher value to handle rare cases when there are more +// than 1000 spaces in a cluster. Hopefully it will cover 99.(9)% of use cases. +// +// In the rest of the edge cases, we will be missing some spaces, but the effect of this will be +// limited by the fact that the aggregation below will sort spaces desc by rules count in them. +// It will return spaces with most of the existing rules, and will not return spaces with fewer +// number of rules. Hopefully, we will miss only spaces with very few rules. This should be +// acceptable because the goal of getting all space ids in the rule monitoring subdomain is to be +// able to aggregate health metrics for those spaces. It's unlikely that spaces with very few rules +// will have a major impact on health and performance metrics of the whole cluster. +const MAX_KIBANA_SPACES = 10_000; + +export const getSpacesAggregation = (): Record< + string, + estypes.AggregationsAggregationContainer +> => { + return { + rulesBySpace: { + terms: { + field: 'alert.namespaces', + size: MAX_KIBANA_SPACES, + }, + }, + }; +}; + +export const normalizeSpacesAggregation = ( + aggregations: Record | undefined +): string[] => { + const rulesBySpace: RawData = aggregations?.rulesBySpace || {}; + const buckets: RawData[] = rulesBySpace.buckets || []; + const spaceIds = buckets.map((b: RawData) => String(b.key)); + return spaceIds; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_spaces/rule_spaces_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_spaces/rule_spaces_client.ts new file mode 100644 index 0000000000000..726d30b518764 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/detection_engine_health/rule_spaces/rule_spaces_client.ts @@ -0,0 +1,51 @@ +/* + * 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 { SavedObjectsClientContract, Logger } from '@kbn/core/server'; +import { RULE_SAVED_OBJECT_TYPE } from '../../event_log/event_log_constants'; +import { DETECTION_RULES_FILTER } from '../rule_objects/filters'; +import { getSpacesAggregation, normalizeSpacesAggregation } from './aggregations/spaces'; + +/** + * Client for getting information about Kibana spaces in the context of detection rules. + */ +export interface IRuleSpacesClient { + /** + * Returns id of the current Kibana space (associated with the current HTTP request). + */ + getCurrentSpaceId(): string; + + /** + * Returns ids of all Kibana spaces where at least one detection rule exists. + */ + getAllSpaceIds(): Promise; +} + +export const createRuleSpacesClient = ( + currentSpaceId: string, + internalSavedObjectsClient: SavedObjectsClientContract, + logger: Logger +): IRuleSpacesClient => { + return { + getCurrentSpaceId(): string { + return currentSpaceId; + }, + + async getAllSpaceIds(): Promise { + const aggs = getSpacesAggregation(); + const response = await internalSavedObjectsClient.find>({ + type: RULE_SAVED_OBJECT_TYPE, // query rules + filter: DETECTION_RULES_FILTER, // make sure to query only detection rules + namespaces: ['*'], // aggregate rules in all Kibana spaces + perPage: 0, // don't return rules in the response, we only need aggs + aggs, + }); + + return normalizeSpacesAggregation(response.aggregations); + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/event_log/event_log_fields.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/event_log/event_log_fields.ts index 06e631dece638..8c47d669c2a2c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/event_log/event_log_fields.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/event_log/event_log_fields.ts @@ -13,6 +13,7 @@ export const TIMESTAMP = `@timestamp` as const; export const MESSAGE = 'message' as const; export const EVENT_PROVIDER = 'event.provider' as const; export const EVENT_ACTION = 'event.action' as const; +export const EVENT_CATEGORY = 'event.category' as const; export const EVENT_SEQUENCE = 'event.sequence' as const; export const LOG_LEVEL = 'log.level' as const; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/service.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/service.ts index b2381d3dbcf26..c92fe66f9cebf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/service.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/service.ts @@ -27,6 +27,7 @@ import { registerEventLogProvider } from './event_log/register_event_log_provide import { createDetectionEngineHealthClient } from './detection_engine_health/detection_engine_health_client'; import { createEventLogHealthClient } from './detection_engine_health/event_log/event_log_health_client'; import { createRuleObjectsHealthClient } from './detection_engine_health/rule_objects/rule_objects_health_client'; +import { createRuleSpacesClient } from './detection_engine_health/rule_spaces/rule_spaces_client'; import { createEventLogReader } from './rule_execution_log/event_log/event_log_reader'; import { createEventLogWriter } from './rule_execution_log/event_log/event_log_writer'; import { fetchRuleExecutionSettings } from './rule_execution_log/execution_settings/fetch_rule_execution_settings'; @@ -71,23 +72,43 @@ export const createRuleMonitoringService = ( const { rulesClient, eventLogClient, currentSpaceId } = params; const { savedObjects } = coreStart; - const ruleObjectsHealthClient = createRuleObjectsHealthClient(rulesClient); - const eventLogHealthClient = createEventLogHealthClient(eventLogClient); - - // Create an importer that can import saved objects on behalf of the internal Kibana user. - // This is important because we want to let users with access to Security Solution - // to be able to install our internal assets like rule monitoring dashboard without - // the need to configure the additional `Saved Objects Management: All` privilege. - const savedObjectsRepository = savedObjects.createInternalRepository(); - const savedObjectsClient = new SavedObjectsClient(savedObjectsRepository); - const savedObjectsImporter = savedObjects.createImporter(savedObjectsClient); + // Create a saved objects client and an importer that can work with saved objects on behalf + // of the internal Kibana user. This is important because we want to let users with access + // to only Security Solution to be able to: + // 1. Install our internal assets like rule monitoring dashboard without the need to + // configure the additional `Saved Objects Management: All` privilege. + // 2. Aggregate rules in all Kibana spaces to get a health overview of the whole cluster - + // without having explicit access to every existing space. + const internalSavedObjectsRepository = savedObjects.createInternalRepository([ + // Note: we include the "alert" hidden SO type here otherwise we would not be able to query it. + // If at some point it is not considered a hidden type this can be removed. + 'alert', + ]); + const internalSavedObjectsClient = new SavedObjectsClient(internalSavedObjectsRepository); + const internalSavedObjectsImporter = savedObjects.createImporter(internalSavedObjectsClient); + + const ruleSpacesClient = createRuleSpacesClient( + currentSpaceId, + internalSavedObjectsClient, + logger + ); + const ruleObjectsHealthClient = createRuleObjectsHealthClient( + rulesClient, + internalSavedObjectsClient, + logger + ); + const eventLogHealthClient = createEventLogHealthClient( + eventLogClient, + ruleSpacesClient, + logger + ); return createDetectionEngineHealthClient( + ruleSpacesClient, ruleObjectsHealthClient, eventLogHealthClient, - savedObjectsImporter, - logger, - currentSpaceId + internalSavedObjectsImporter, + logger ); }, diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts index 113c10cf2d910..6aa483cf7f785 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts @@ -108,7 +108,9 @@ const assertFilterControlsWithFilterObject = ( }); }; -describe(`Detections : Page Filters`, { tags: ['@ess', '@serverless'] }, () => { +// Failing: See https://github.com/elastic/kibana/issues/167914 +// Failing: See https://github.com/elastic/kibana/issues/167915 +describe.skip(`Detections : Page Filters`, { tags: ['@ess', '@serverless'] }, () => { before(() => { cleanKibana(); createRule(getNewRule({ rule_id: 'custom_rule_filters' })); diff --git a/yarn.lock b/yarn.lock index b33dbc1c563a3..76ac5b038ee8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22684,7 +22684,7 @@ nanoid@3.3.3: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== -nanoid@^3.3.1, nanoid@^3.3.4: +nanoid@^3.3.1: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== @@ -24621,19 +24621,10 @@ postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0 picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.4.14: - version "8.4.14" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" - integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== - dependencies: - nanoid "^3.3.4" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -postcss@^8.4.23: - version "8.4.25" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.25.tgz#4a133f5e379eda7f61e906c3b1aaa9b81292726f" - integrity sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw== +postcss@^8.4.14, postcss@^8.4.23, postcss@^8.4.31: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: nanoid "^3.3.6" picocolors "^1.0.0"