From 3fa1750e6a02de62f48be01aacd31f22345a211c Mon Sep 17 00:00:00 2001 From: Vadim Yakhin Date: Thu, 9 Dec 2021 11:25:08 -0800 Subject: [PATCH 001/145] Improve github apps frontend validation (#120983) by disabling the submit button if private key has not been uploaded Note: the validation will not work if a user removes the file from the file picker after uploading it, as file picker doesn't call the onChange callback on that action. --- .../components/add_source/github_via_app.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx index a08f49b8bbe78..b62648348ed80 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx @@ -44,8 +44,13 @@ interface GithubViaAppProps { export const GitHubViaApp: React.FC = ({ isGithubEnterpriseServer }) => { const { isOrganization } = useValues(AppLogic); - const { githubAppId, githubEnterpriseServerUrl, isSubmitButtonLoading, indexPermissionsValue } = - useValues(GithubViaAppLogic); + const { + githubAppId, + githubEnterpriseServerUrl, + stagedPrivateKey, + isSubmitButtonLoading, + indexPermissionsValue, + } = useValues(GithubViaAppLogic); const { setGithubAppId, setGithubEnterpriseServerUrl, @@ -118,7 +123,12 @@ export const GitHubViaApp: React.FC = ({ isGithubEnterpriseSe fill type="submit" isLoading={isSubmitButtonLoading} - isDisabled={!githubAppId || (isGithubEnterpriseServer && !githubEnterpriseServerUrl)} + isDisabled={ + // disable submit button if any required fields are empty + !githubAppId || + (isGithubEnterpriseServer && !githubEnterpriseServerUrl) || + !stagedPrivateKey + } > {isSubmitButtonLoading ? 'Connecting…' : `Connect ${name}`} From bd77e4f681b87aee580bec36028958b0176cceef Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 9 Dec 2021 15:00:19 -0500 Subject: [PATCH 002/145] [Fleet] Add CA fingerprint field to the output form (#120980) --- .../components/edit_output_flyout/index.tsx | 21 ++++++++++++++ .../output_form_validators.tsx | 10 +++++++ .../edit_output_flyout/use_output_form.tsx | 28 ++++++++++++++++--- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx index 824eec081e28b..62b22d0bdffc6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx @@ -135,6 +135,27 @@ export const EditOutputFlyout: React.FunctionComponent = })} {...inputs.elasticsearchUrlInput.props} /> + + } + {...inputs.caTrustedFingerprintInput.formRowProps} + > + + ( void, output?: Output) { isPreconfigured ); + const caTrustedFingerprintInput = useInput( + output?.ca_trusted_fingerprint ?? '', + validateCATrustedFingerPrint, + isPreconfigured + ); + const defaultOutputInput = useSwitchInput( output?.is_default ?? false, isPreconfigured || output?.is_default @@ -127,6 +138,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { additionalYamlConfigInput, defaultOutputInput, defaultMonitoringOutputInput, + caTrustedFingerprintInput, }; const hasChanged = Object.values(inputs).some((input) => input.hasChanged); @@ -135,13 +147,19 @@ export function useOutputForm(onSucess: () => void, output?: Output) { const nameInputValid = nameInput.validate(); const elasticsearchUrlsValid = elasticsearchUrlInput.validate(); const additionalYamlConfigValid = additionalYamlConfigInput.validate(); - - if (!elasticsearchUrlsValid || !additionalYamlConfigValid || !nameInputValid) { + const caTrustedFingerprintValid = caTrustedFingerprintInput.validate(); + + if ( + !elasticsearchUrlsValid || + !additionalYamlConfigValid || + !nameInputValid || + !caTrustedFingerprintValid + ) { return false; } return true; - }, [nameInput, elasticsearchUrlInput, additionalYamlConfigInput]); + }, [nameInput, elasticsearchUrlInput, additionalYamlConfigInput, caTrustedFingerprintInput]); const submit = useCallback(async () => { try { @@ -157,6 +175,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { is_default: defaultOutputInput.value, is_default_monitoring: defaultMonitoringOutputInput.value, config_yaml: additionalYamlConfigInput.value, + ca_trusted_fingerprint: caTrustedFingerprintInput.value, }; if (output) { @@ -195,6 +214,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { defaultMonitoringOutputInput.value, defaultOutputInput.value, elasticsearchUrlInput.value, + caTrustedFingerprintInput.value, nameInput.value, notifications.toasts, onSucess, From c72d6b46c3dd904626a562e32cbef8c05dd5d25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 9 Dec 2021 15:11:02 -0500 Subject: [PATCH 003/145] [APM] disable fleet depreaction warning (#120988) --- x-pack/plugins/apm/server/plugin.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index b603d9e72a2b0..2f8e10d68ae51 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -47,7 +47,6 @@ import { TRANSACTION_TYPE, } from '../common/elasticsearch_fieldnames'; import { tutorialProvider } from './tutorial'; -import { getDeprecations } from './deprecations'; export class APMPlugin implements @@ -197,14 +196,6 @@ export class APMPlugin kibanaVersion: this.initContext.env.packageInfo.version, }); - core.deprecations.registerDeprecations({ - getDeprecations: getDeprecations({ - cloudSetup: plugins.cloud, - fleet: resourcePlugins.fleet, - branch: this.initContext.env.packageInfo.branch, - }), - }); - return { config$, getApmIndices: boundGetApmIndices, From 744849f491b201c5bd4a0715894ff17b0086f3c2 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Thu, 9 Dec 2021 13:23:27 -0800 Subject: [PATCH 004/145] Support system indices being hidden (#120985) Co-authored-by: Josh Dover --- packages/kbn-es-archiver/src/actions/load.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/kbn-es-archiver/src/actions/load.ts b/packages/kbn-es-archiver/src/actions/load.ts index c5bea5e29a687..0a318f895deb3 100644 --- a/packages/kbn-es-archiver/src/actions/load.ts +++ b/packages/kbn-es-archiver/src/actions/load.ts @@ -86,15 +86,17 @@ export async function loadAction({ progress.deactivate(); const result = stats.toJSON(); + const indicesWithDocs: string[] = []; for (const [index, { docs }] of Object.entries(result)) { if (docs && docs.indexed > 0) { log.info('[%s] Indexed %d docs into %j', name, docs.indexed, index); + indicesWithDocs.push(index); } } await client.indices.refresh( { - index: '_all', + index: indicesWithDocs.join(','), allow_no_indices: true, }, { From d8d48fe6568682d899fd4744815e2bab5f82fb3d Mon Sep 17 00:00:00 2001 From: vladpro25 <91911546+vladpro25@users.noreply.github.com> Date: Thu, 9 Dec 2021 23:47:04 +0200 Subject: [PATCH 005/145] Auto complete for script suggests deprecated query type (#120283) * Change suggestions for Sampler and Diversified sampler aggregations * Auto complete for script suggests deprecated query type * Auto complete for script suggests deprecated query type * Auto complete for script suggests deprecated query type * Auto complete for script suggests deprecated query type Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/spec_definitions/js/query/dsl.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/plugins/console/server/lib/spec_definitions/js/query/dsl.ts b/src/plugins/console/server/lib/spec_definitions/js/query/dsl.ts index 50da221c7e6bb..895d2e4ed72fe 100644 --- a/src/plugins/console/server/lib/spec_definitions/js/query/dsl.ts +++ b/src/plugins/console/server/lib/spec_definitions/js/query/dsl.ts @@ -745,6 +745,16 @@ export const query = (specService: SpecDefinitionsService) => { // populated by a global rule }, }, + script_score: { + __template: { + script: {}, + query: {}, + }, + script: {}, + query: {}, + min_score: '', + boost: 1.0, + }, wrapper: { __template: { query: 'QUERY_BASE64_ENCODED', From 21af67080d1bbecbf8175e397b61f63eb7080223 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 9 Dec 2021 14:48:57 -0700 Subject: [PATCH 006/145] [Rule Registry] Switch to _source for updating documents instead of Fields API (#118245) * [Rule Registry] Switch to _source for updating documents instead of Fields API * updating test with _source instead of fields * removing mapValues dep * Refactor types and clean up names Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/rule_data_client/types.ts | 6 ++---- .../utils/create_lifecycle_executor.test.ts | 8 ++++---- .../server/utils/create_lifecycle_executor.ts | 18 ++++++++---------- .../utils/create_lifecycle_rule_type.test.ts | 14 +++----------- 4 files changed, 17 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts index 5fab32eb38868..e970a13c78aaa 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts @@ -10,7 +10,7 @@ import { BulkRequest, BulkResponse } from '@elastic/elasticsearch/lib/api/typesW import { ESSearchRequest, ESSearchResponse } from 'src/core/types/elasticsearch'; import { FieldDescriptor } from 'src/plugins/data/server'; -import { TechnicalRuleDataFieldName } from '../../common/technical_rule_data_field_names'; +import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; export interface IRuleDataClient { indexName: string; @@ -24,9 +24,7 @@ export interface IRuleDataClient { export interface IRuleDataReader { search( request: TSearchRequest - ): Promise< - ESSearchResponse>, TSearchRequest> - >; + ): Promise, TSearchRequest>>; getDynamicIndexPattern(target?: string): Promise<{ title: string; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts index 2c5fe09d80563..d1c20e0667e24 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts @@ -127,7 +127,7 @@ describe('createLifecycleExecutor', () => { hits: { hits: [ { - fields: { + _source: { '@timestamp': '', [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_UUID]: 'ALERT_0_UUID', @@ -144,7 +144,7 @@ describe('createLifecycleExecutor', () => { }, }, { - fields: { + _source: { '@timestamp': '', [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_UUID]: 'ALERT_1_UUID', @@ -247,7 +247,7 @@ describe('createLifecycleExecutor', () => { hits: { hits: [ { - fields: { + _source: { '@timestamp': '', [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', [ALERT_UUID]: 'ALERT_0_UUID', @@ -263,7 +263,7 @@ describe('createLifecycleExecutor', () => { }, }, { - fields: { + _source: { '@timestamp': '', [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', [ALERT_UUID]: 'ALERT_1_UUID', diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index c30b1654a3587..0ca0002470af0 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -18,7 +18,7 @@ import { AlertTypeParams, AlertTypeState, } from '../../../alerting/server'; -import { ParsedTechnicalFields, parseTechnicalFields } from '../../common/parse_technical_fields'; +import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; import { ALERT_DURATION, ALERT_END, @@ -216,8 +216,6 @@ export const createLifecycleExecutor = collapse: { field: ALERT_UUID, }, - _source: false, - fields: [{ field: '*', include_unmapped: true }], sort: { [TIMESTAMP]: 'desc' as const, }, @@ -226,13 +224,13 @@ export const createLifecycleExecutor = }); hits.hits.forEach((hit) => { - const fields = parseTechnicalFields(hit.fields); - const indexName = hit._index; - const alertId = fields[ALERT_INSTANCE_ID]; - trackedAlertsDataMap[alertId] = { - indexName, - fields, - }; + const alertId = hit._source[ALERT_INSTANCE_ID]; + if (alertId) { + trackedAlertsDataMap[alertId] = { + indexName: hit._index, + fields: hit._source, + }; + } }); } diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index f0e2412629bb1..7aa7dcd9620fe 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -14,7 +14,7 @@ import { ALERT_UUID, } from '@kbn/rule-data-utils'; import { loggerMock } from '@kbn/logging/mocks'; -import { castArray, omit, mapValues } from 'lodash'; +import { castArray, omit } from 'lodash'; import { RuleDataClient } from '../rule_data_client'; import { createRuleDataClientMock } from '../rule_data_client/rule_data_client.mock'; import { createLifecycleRuleTypeFactory } from './create_lifecycle_rule_type_factory'; @@ -293,14 +293,10 @@ describe('createLifecycleRuleTypeFactory', () => { (doc: any) => !('index' in doc) && doc['service.name'] === 'opbeans-node' ) as Record; - const stored = mapValues(lastOpbeansNodeDoc, (val) => { - return castArray(val); - }); - // @ts-ignore 4.3.5 upgrade helpers.ruleDataClientMock.getReader().search.mockResolvedValueOnce({ hits: { - hits: [{ fields: stored } as any], + hits: [{ _source: lastOpbeansNodeDoc } as any], total: { value: 1, relation: 'eq', @@ -378,13 +374,9 @@ describe('createLifecycleRuleTypeFactory', () => { (doc: any) => !('index' in doc) && doc['service.name'] === 'opbeans-node' ) as Record; - const stored = mapValues(lastOpbeansNodeDoc, (val) => { - return castArray(val); - }); - helpers.ruleDataClientMock.getReader().search.mockResolvedValueOnce({ hits: { - hits: [{ fields: stored } as any], + hits: [{ _source: lastOpbeansNodeDoc } as any], total: { value: 1, relation: 'eq', From 63f58dad17129b6b95c720e55596839b63f614ff Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 9 Dec 2021 16:07:26 -0600 Subject: [PATCH 007/145] [build/docker] Use /tmp to store kibana archive (#120991) --- .../os_packages/docker_generator/templates/base/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile index b1d9fafffab57..90a622e64efe4 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile @@ -16,7 +16,7 @@ RUN {{packageManager}} install -y findutils tar gzip {{/ubi}} {{#usePublicArtifact}} -RUN cd /opt && \ +RUN cd /tmp && \ curl --retry 8 -s -L \ --output kibana.tar.gz \ https://artifacts.elastic.co/downloads/kibana/{{artifactPrefix}}-$(arch).tar.gz && \ From 34711539fa61c5add55032e97a379961cc9d4706 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 9 Dec 2021 15:57:30 -0700 Subject: [PATCH 008/145] Fixes alerts and cases to work with telemetry (#121002) ## Summary One line fix to where we have to expose cases to the saved object client as hidden to work with telemetry. This one liner was broken out from: https://github.com/elastic/kibana/pull/120809 So we could back-port easier to earlier versions. Manual testing: To see telemetry go to advanced settings -> Usage Data (and click cluster data): Screen Shot 2021-12-08 at 4 14 43 PM And you will see it like so: Screen Shot 2021-12-08 at 4 43 10 PM --- x-pack/plugins/security_solution/server/usage/collector.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index 5402dd4c375a8..3f64a6e5e227b 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -9,6 +9,7 @@ import { CoreSetup, SavedObjectsClientContract } from '../../../../../src/core/s import { CollectorFetchContext } from '../../../../../src/plugins/usage_collection/server'; import { CollectorDependencies } from './types'; import { fetchDetectionsMetrics } from './detections'; +import { SAVED_OBJECT_TYPES } from '../../../cases/common/constants'; export type RegisterCollector = (deps: CollectorDependencies) => void; export interface UsageData { @@ -17,7 +18,8 @@ export interface UsageData { export async function getInternalSavedObjectsClient(core: CoreSetup) { return core.getStartServices().then(async ([coreStart]) => { - return coreStart.savedObjects.createInternalRepository(); + // note: we include the cases hidden types here otherwise we would not be able to query them. If at some point cases is not considered a hidden type this can be removed + return coreStart.savedObjects.createInternalRepository(SAVED_OBJECT_TYPES); }); } From 5e34758d43884106cb2411433a5324c92ed3b741 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 9 Dec 2021 18:26:59 -0600 Subject: [PATCH 009/145] [Workplace Search] Add a download diagnostics button when error is shown (#121017) * Factor out DownloadDiagnosticsButton for reuse * Add logic for rendering button * Add button to layout * Fix test name * Better method naming --- .../download_diagnostics_button.test.tsx | 51 +++++++++++++++++++ .../download_diagnostics_button.tsx | 48 +++++++++++++++++ .../components/source_layout.test.tsx | 12 +++++ .../components/source_layout.tsx | 12 ++++- .../components/source_settings.test.tsx | 34 +------------ .../components/source_settings.tsx | 21 ++------ .../views/content_sources/constants.ts | 7 +++ .../content_sources/source_logic.test.ts | 28 ++++++++++ .../views/content_sources/source_logic.ts | 18 +++++++ 9 files changed, 180 insertions(+), 51 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/download_diagnostics_button.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/download_diagnostics_button.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/download_diagnostics_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/download_diagnostics_button.test.tsx new file mode 100644 index 0000000000000..ca2af637c1d6e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/download_diagnostics_button.test.tsx @@ -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 { setMockValues } from '../../../../__mocks__/kea_logic'; +import { fullContentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton } from '@elastic/eui'; + +import { DownloadDiagnosticsButton } from './download_diagnostics_button'; + +describe('DownloadDiagnosticsButton', () => { + const label = 'foo123'; + const contentSource = fullContentSources[0]; + const buttonLoading = false; + const isOrganization = true; + + const mockValues = { + contentSource, + buttonLoading, + isOrganization, + }; + + beforeEach(() => { + setMockValues(mockValues); + }); + + it('renders the Download diagnostics button with org href', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiButton).prop('href')).toEqual( + '/internal/workplace_search/org/sources/123/download_diagnostics' + ); + }); + + it('renders the Download diagnostics button with account href', () => { + setMockValues({ ...mockValues, isOrganization: false }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButton).prop('href')).toEqual( + '/internal/workplace_search/account/sources/123/download_diagnostics' + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/download_diagnostics_button.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/download_diagnostics_button.tsx new file mode 100644 index 0000000000000..866746f43d653 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/download_diagnostics_button.tsx @@ -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 React from 'react'; + +import { useValues } from 'kea'; + +import { EuiButton } from '@elastic/eui'; + +import { HttpLogic } from '../../../../shared/http'; +import { AppLogic } from '../../../app_logic'; + +import { SourceLogic } from '../source_logic'; + +interface Props { + label: string; +} + +export const DownloadDiagnosticsButton: React.FC = ({ label }) => { + const { http } = useValues(HttpLogic); + const { isOrganization } = useValues(AppLogic); + const { + contentSource: { id, serviceType }, + buttonLoading, + } = useValues(SourceLogic); + + const diagnosticsPath = isOrganization + ? http.basePath.prepend(`/internal/workplace_search/org/sources/${id}/download_diagnostics`) + : http.basePath.prepend( + `/internal/workplace_search/account/sources/${id}/download_diagnostics` + ); + + return ( + + {label} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx index 944a54169f0b8..62d1bff27dd78 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.test.tsx @@ -18,6 +18,7 @@ import { EuiCallOut } from '@elastic/eui'; import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../../components/layout'; +import { DownloadDiagnosticsButton } from './download_diagnostics_button'; import { SourceInfoCard } from './source_info_card'; import { SourceLayout } from './source_layout'; @@ -26,6 +27,7 @@ describe('SourceLayout', () => { const mockValues = { contentSource, dataLoading: false, + diagnosticDownloadButtonVisible: false, isOrganization: true, }; @@ -87,4 +89,14 @@ describe('SourceLayout', () => { expect(wrapper.find(EuiCallOut)).toHaveLength(1); }); + + it('renders DownloadDiagnosticsButton', () => { + setMockValues({ + ...mockValues, + diagnosticDownloadButtonVisible: true, + }); + const wrapper = shallow(); + + expect(wrapper.find(DownloadDiagnosticsButton)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx index f741cfdc538fc..727e171d1073c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx @@ -19,12 +19,14 @@ import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../../c import { NAV } from '../../../constants'; import { + DOWNLOAD_DIAGNOSTIC_BUTTON, SOURCE_DISABLED_CALLOUT_TITLE, SOURCE_DISABLED_CALLOUT_DESCRIPTION, SOURCE_DISABLED_CALLOUT_BUTTON, } from '../constants'; import { SourceLogic } from '../source_logic'; +import { DownloadDiagnosticsButton } from './download_diagnostics_button'; import { SourceInfoCard } from './source_info_card'; export const SourceLayout: React.FC = ({ @@ -32,7 +34,7 @@ export const SourceLayout: React.FC = ({ pageChrome = [], ...props }) => { - const { contentSource, dataLoading } = useValues(SourceLogic); + const { contentSource, dataLoading, diagnosticDownloadButtonVisible } = useValues(SourceLogic); const { isOrganization } = useValues(AppLogic); const { name, createdAt, serviceType, isFederatedSource, supportedByLicense } = contentSource; @@ -61,6 +63,13 @@ export const SourceLayout: React.FC = ({ ); + const downloadDiagnosticButton = ( + <> + + + + ); + const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; return ( @@ -69,6 +78,7 @@ export const SourceLayout: React.FC = ({ {...props} pageChrome={[NAV.SOURCES, name || '...', ...pageChrome]} > + {diagnosticDownloadButtonVisible && downloadDiagnosticButton} {!supportedByLicense && callout} {pageHeader} {children} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx index 83cf21ce86233..ec499293f2fd1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx @@ -18,6 +18,7 @@ import { EuiConfirmModal } from '@elastic/eui'; import { SourceConfigFields } from '../../../components/shared/source_config_fields'; +import { DownloadDiagnosticsButton } from './download_diagnostics_button'; import { SourceSettings } from './source_settings'; describe('SourceSettings', () => { @@ -48,6 +49,7 @@ describe('SourceSettings', () => { const wrapper = shallow(); expect(wrapper.find('form')).toHaveLength(1); + expect(wrapper.find(DownloadDiagnosticsButton)).toHaveLength(1); }); it('handles form submission', () => { @@ -104,36 +106,4 @@ describe('SourceSettings', () => { sourceConfigData.configuredFields.publicKey ); }); - - describe('DownloadDiagnosticsButton', () => { - it('renders for org with correct href', () => { - const wrapper = shallow(); - - expect(wrapper.find('[data-test-subj="DownloadDiagnosticsButton"]').prop('href')).toEqual( - '/internal/workplace_search/org/sources/123/download_diagnostics' - ); - }); - - it('renders for account with correct href', () => { - setMockValues({ - ...mockValues, - isOrganization: false, - }); - const wrapper = shallow(); - - expect(wrapper.find('[data-test-subj="DownloadDiagnosticsButton"]').prop('href')).toEqual( - '/internal/workplace_search/account/sources/123/download_diagnostics' - ); - }); - - it('renders with the correct download file name', () => { - jest.spyOn(global.Date, 'now').mockImplementationOnce(() => new Date('1970-01-01').valueOf()); - - const wrapper = shallow(); - - expect(wrapper.find('[data-test-subj="DownloadDiagnosticsButton"]').prop('download')).toEqual( - '123_custom_0_diagnostics.json' - ); - }); - }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index e5924b672c771..484a9ca14b4e1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -23,7 +23,6 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { HttpLogic } from '../../../../shared/http'; import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; import { AppLogic } from '../../../app_logic'; import { ContentSection } from '../../../components/shared/content_section'; @@ -61,11 +60,11 @@ import { import { staticSourceData } from '../source_data'; import { SourceLogic } from '../source_logic'; +import { DownloadDiagnosticsButton } from './download_diagnostics_button'; + import { SourceLayout } from './source_layout'; export const SourceSettings: React.FC = () => { - const { http } = useValues(HttpLogic); - const { updateContentSource, removeContentSource, @@ -110,12 +109,6 @@ export const SourceSettings: React.FC = () => { const { clientId, clientSecret, publicKey, consumerKey, baseUrl } = configuredFields || {}; - const diagnosticsPath = isOrganization - ? http.basePath.prepend(`/internal/workplace_search/org/sources/${id}/download_diagnostics`) - : http.basePath.prepend( - `/internal/workplace_search/account/sources/${id}/download_diagnostics` - ); - const handleNameChange = (e: ChangeEvent) => setValue(e.target.value); const submitNameChange = (e: FormEvent) => { @@ -241,15 +234,7 @@ export const SourceSettings: React.FC = () => { )} - - {SYNC_DIAGNOSTICS_BUTTON} - + { dataLoading: true, sectionLoading: true, buttonLoading: false, + diagnosticDownloadButtonVisible: false, contentMeta: DEFAULT_META, contentFilterValue: '', isConfigurationUpdateButtonLoading: false, @@ -125,6 +126,12 @@ describe('SourceLogic', () => { expect(SourceLogic.values.buttonLoading).toEqual(false); }); + + it('showDiagnosticDownloadButton', () => { + SourceLogic.actions.showDiagnosticDownloadButton(); + + expect(SourceLogic.values.diagnosticDownloadButtonVisible).toEqual(true); + }); }); describe('listeners', () => { @@ -183,6 +190,27 @@ describe('SourceLogic', () => { expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); + it('handles error message with diagnostic bundle error message', async () => { + const showDiagnosticDownloadButtonSpy = jest.spyOn( + SourceLogic.actions, + 'showDiagnosticDownloadButton' + ); + + // For contenst source errors, the API returns the source errors in an error property in the success + // response. We don't reject here because we still render the content source with the error. + const promise = Promise.resolve({ + ...contentSource, + errors: [ + 'The database is on fire. [Check diagnostic bundle for details - Message id: 123]', + ], + }); + http.get.mockReturnValue(promise); + SourceLogic.actions.initializeSource(contentSource.id); + await promise; + + expect(showDiagnosticDownloadButtonSpy).toHaveBeenCalled(); + }); + describe('404s', () => { const mock404 = Promise.reject({ response: { status: 404 } }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index b76627f57b3a3..8f0cfa8cfa280 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -52,6 +52,7 @@ export interface SourceActions { setButtonNotLoading(): void; setStagedPrivateKey(stagedPrivateKey: string | null): string | null; setConfigurationUpdateButtonNotLoading(): void; + showDiagnosticDownloadButton(): void; } interface SourceValues { @@ -59,6 +60,7 @@ interface SourceValues { dataLoading: boolean; sectionLoading: boolean; buttonLoading: boolean; + diagnosticDownloadButtonVisible: boolean; contentItems: SourceContentItem[]; contentMeta: Meta; contentFilterValue: string; @@ -108,6 +110,7 @@ export const SourceLogic = kea>({ setButtonNotLoading: () => false, setStagedPrivateKey: (stagedPrivateKey: string) => stagedPrivateKey, setConfigurationUpdateButtonNotLoading: () => false, + showDiagnosticDownloadButton: true, }, reducers: { contentSource: [ @@ -147,6 +150,13 @@ export const SourceLogic = kea>({ setSearchResults: () => false, }, ], + diagnosticDownloadButtonVisible: [ + false, + { + showDiagnosticDownloadButton: () => true, + initializeSource: () => false, + }, + ], contentItems: [ [], { @@ -200,6 +210,9 @@ export const SourceLogic = kea>({ } if (response.errors) { setErrorMessage(response.errors); + if (errorsHaveDiagnosticBundleString(response.errors as unknown as string[])) { + actions.showDiagnosticDownloadButton(); + } } else { clearFlashMessages(); } @@ -343,3 +356,8 @@ const setPage = (state: Meta, page: number) => ({ current: page, }, }); + +const errorsHaveDiagnosticBundleString = (errors: string[]) => { + const ERROR_SUBSTRING = 'Check diagnostic bundle for details'; + return errors.find((e) => e.includes(ERROR_SUBSTRING)); +}; From 181b9e0271966d13e2f2b1097e8fac351135c74b Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 9 Dec 2021 17:56:42 -0700 Subject: [PATCH 010/145] [Security Solutions] Fixes telemetry to work with rule types (#120809) ## Summary What this does: * Fixes telemetry to work with the newer rule types * Updates the queries to the new rule types and rule query names * Uses constants where I can from cases and the new rule types * Changes the index to the new index type alias * Adds e2e backend tests we didn't have before What this does not do: * Doesn't add e2e backend tests for alerts added to cases * Doesn't add e2e backend tests for ML jobs for security_solution Those two test scenarios have to be manually tested still. Manual testing: To see telemetry go to advanced settings -> Usage Data (and click cluster data): Screen Shot 2021-12-08 at 4 14 43 PM Create alerts of different types and add them to cases: Screen Shot 2021-12-08 at 4 48 21 PM Activate ML_jobs and any alerts that have ML jobs associated: Screen Shot 2021-12-08 at 5 08 42 PM When clicking advanced settings -> Usage Data -> Click cluster data Search for `security_solution` and then ensure that the data looks as expected underneath the different values such as: `ml_jobs` Screen Shot 2021-12-08 at 3 08 25 PM `detection_rules` and `cases` working again: Screen Shot 2021-12-08 at 4 43 10 PM Note, `detection_rule_detail` will only be filled in if have prepackaged rules installed: Screen Shot 2021-12-08 at 5 14 50 PM Also note that the `detection_rule_detail`'s `rule_id` is its UUID and not its `rule_id`. That's the way it's been in the codebase for a while it looks like so I have not changed that behavior. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../security_solution/server/plugin.ts | 9 +- .../detections/detection_rule_helpers.ts | 42 +- .../usage/detections/detections.test.ts | 4 +- .../server/usage/detections/types.ts | 2 +- .../security_and_spaces/tests/index.ts | 4 + .../tests/telemetry/README.md | 3 + .../tests/telemetry/detection_rules.ts | 426 ++++++++++++++++++ .../tests/telemetry/index.ts | 18 + .../detection_engine_api_integration/utils.ts | 150 ++++++ .../es_archives/security_solution/README.md | 2 + .../security_solution/telemetry/data.json | 51 +++ .../security_solution/telemetry/mappings.json | 20 + 12 files changed, 718 insertions(+), 13 deletions(-) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/detection_rules.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts create mode 100644 x-pack/test/functional/es_archives/security_solution/telemetry/data.json create mode 100644 x-pack/test/functional/es_archives/security_solution/telemetry/mappings.json diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 75933afca9686..a676ca8779f6a 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -45,7 +45,12 @@ import { initSavedObjects } from './saved_objects'; import { AppClientFactory } from './client'; import { createConfig, ConfigType } from './config'; import { initUiSettings } from './ui_settings'; -import { APP_ID, SERVER_APP_ID, LEGACY_NOTIFICATIONS_ID } from '../common/constants'; +import { + APP_ID, + SERVER_APP_ID, + LEGACY_NOTIFICATIONS_ID, + DEFAULT_ALERTS_INDEX, +} from '../common/constants'; import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency'; import { registerResolverRoutes } from './endpoint/routes/resolver'; @@ -161,7 +166,7 @@ export class Plugin implements ISecuritySolutionPlugin { initUsageCollectors({ core, kibanaIndex: core.savedObjects.getKibanaIndex(), - signalsIndex: config.signalsIndex, + signalsIndex: DEFAULT_ALERTS_INDEX, ml: plugins.ml, usageCollection: plugins.usageCollection, }); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts index a85f70d5a328d..0eb2a03e0c3a2 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts @@ -5,7 +5,17 @@ * 2.0. */ -import { SIGNALS_ID } from '@kbn/securitysolution-rules'; +import { + SIGNALS_ID, + EQL_RULE_TYPE_ID, + INDICATOR_RULE_TYPE_ID, + ML_RULE_TYPE_ID, + QUERY_RULE_TYPE_ID, + THRESHOLD_RULE_TYPE_ID, + SAVED_QUERY_RULE_TYPE_ID, +} from '@kbn/securitysolution-rules'; +import { ALERT_RULE_UUID } from '@kbn/rule-data-utils'; +import { CASE_COMMENT_SAVED_OBJECT } from '../../../../cases/common/constants'; import { ElasticsearchClient, SavedObjectsClientContract } from '../../../../../../src/core/server'; import { isElasticRule } from './index'; @@ -188,7 +198,25 @@ export const getDetectionRuleMetrics = async ( ): Promise => { let rulesUsage: DetectionRulesTypeUsage = initialDetectionRulesUsage; const ruleSearchOptions: RuleSearchParams = { - body: { query: { bool: { filter: { term: { 'alert.alertTypeId': SIGNALS_ID } } } } }, + body: { + query: { + bool: { + filter: { + terms: { + 'alert.alertTypeId': [ + SIGNALS_ID, + EQL_RULE_TYPE_ID, + ML_RULE_TYPE_ID, + QUERY_RULE_TYPE_ID, + SAVED_QUERY_RULE_TYPE_ID, + INDICATOR_RULE_TYPE_ID, + THRESHOLD_RULE_TYPE_ID, + ], + }, + }, + }, + }, + }, filter_path: [], ignore_unavailable: true, index: kibanaIndex, @@ -203,7 +231,7 @@ export const getDetectionRuleMetrics = async ( body: { aggs: { detectionAlerts: { - terms: { field: 'signal.rule.id.keyword' }, + terms: { field: ALERT_RULE_UUID }, }, }, query: { @@ -224,11 +252,10 @@ export const getDetectionRuleMetrics = async ( })) as { body: AlertsAggregationResponse }; const cases = await savedObjectClient.find({ - type: 'cases-comments', - fields: [], + type: CASE_COMMENT_SAVED_OBJECT, page: 1, perPage: MAX_RESULTS_WINDOW, - filter: 'cases-comments.attributes.type: alert', + filter: `${CASE_COMMENT_SAVED_OBJECT}.attributes.type: alert`, }); const casesCache = cases.saved_objects.reduce((cache, { attributes: casesObject }) => { @@ -247,14 +274,13 @@ export const getDetectionRuleMetrics = async ( const alertsCache = new Map(); alertBuckets.map((bucket) => alertsCache.set(bucket.key, bucket.doc_count)); - if (ruleResults.hits?.hits?.length > 0) { const ruleObjects = ruleResults.hits.hits.map((hit) => { const ruleId = hit._id.split(':')[1]; const isElastic = isElasticRule(hit._source?.alert.tags); return { rule_name: hit._source?.alert.name, - rule_id: ruleId, + rule_id: hit._source?.alert.params.ruleId, rule_type: hit._source?.alert.params.type, rule_version: hit._source?.alert.params.version, enabled: hit._source?.alert.enabled, diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts index 86c77f8febaf6..3e60691011fe9 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts @@ -111,7 +111,7 @@ describe('Detections Usage and Metrics', () => { created_on: '2021-03-23T17:15:59.634Z', elastic_rule: true, enabled: false, - rule_id: '6eecd8c2-8bfb-11eb-afbe-1b7a66309c6d', + rule_id: '5370d4cd-2bb3-4d71-abf5-1e1d0ff5a2de', rule_name: 'Azure Diagnostic Settings Deletion', rule_type: 'query', rule_version: 4, @@ -248,7 +248,7 @@ describe('Detections Usage and Metrics', () => { created_on: '2021-03-23T17:15:59.634Z', elastic_rule: true, enabled: false, - rule_id: '6eecd8c2-8bfb-11eb-afbe-1b7a66309c6d', + rule_id: '5370d4cd-2bb3-4d71-abf5-1e1d0ff5a2de', rule_name: 'Azure Diagnostic Settings Deletion', rule_type: 'query', rule_version: 4, diff --git a/x-pack/plugins/security_solution/server/usage/detections/types.ts b/x-pack/plugins/security_solution/server/usage/detections/types.ts index 430a524f3f03a..635ff47d0067a 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/types.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/types.ts @@ -9,7 +9,7 @@ interface RuleSearchBody { query: { bool: { filter: { - term: { [key: string]: string }; + terms: { [key: string]: string[] }; }; }; }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index 23cb9b7d9c42c..55b7327670631 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -68,5 +68,9 @@ export default ({ loadTestFile }: FtrProviderContext): void => { describe('', function () { loadTestFile(require.resolve('./alerts/index')); }); + + describe('', function () { + loadTestFile(require.resolve('./telemetry/index')); + }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md new file mode 100644 index 0000000000000..6035675db4349 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md @@ -0,0 +1,3 @@ +These are tests for the telemetry rules within "security_solution/server/usage" +* detection_rules + diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/detection_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/detection_rules.ts new file mode 100644 index 0000000000000..a3dfaea64d2d4 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/detection_rules.ts @@ -0,0 +1,426 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { DetectionMetrics } from '../../../../../plugins/security_solution/server/usage/detections/types'; +import { + EqlCreateSchema, + QueryCreateSchema, + ThreatMatchCreateSchema, + ThresholdCreateSchema, +} from '../../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createRule, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getEqlRuleForSignalTesting, + getInitialDetectionMetrics, + getRuleForSignalTesting, + getSimpleMlRule, + getSimpleThreatMatch, + getStats, + getThresholdRuleForSignalTesting, + installPrePackagedRules, + waitForRuleSuccessOrStatus, + waitForSignalsToBePresent, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const log = getService('log'); + const retry = getService('retry'); + + describe('Detection rule telemetry', async () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/telemetry'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/telemetry'); + }); + + beforeEach(async () => { + await createSignalsIndex(supertest, log); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + }); + + it('should have initialized empty/zero values when no rules are running', async () => { + await retry.try(async () => { + const stats = await getStats(supertest, log); + expect(stats).to.eql(getInitialDetectionMetrics()); + }); + }); + + describe('"kql" rule type', () => { + it('should show stats for active rule', async () => { + const rule: QueryCreateSchema = getRuleForSignalTesting(['telemetry']); + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + query: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + enabled: 1, + alerts: 4, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + alerts: 4, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show stats for in-active rule', async () => { + const rule: QueryCreateSchema = getRuleForSignalTesting(['telemetry'], 'rule-1', false); + await createRule(supertest, log, rule); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + query: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + disabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + }); + + describe('"eql" rule type', () => { + it('should show stats for active rule', async () => { + const rule: EqlCreateSchema = getEqlRuleForSignalTesting(['telemetry']); + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + eql: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + enabled: 1, + alerts: 4, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + alerts: 4, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show stats for in-active rule', async () => { + const rule: EqlCreateSchema = getEqlRuleForSignalTesting(['telemetry'], 'rule-1', false); + await createRule(supertest, log, rule); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + eql: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + disabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + }); + + describe('"threshold" rule type', () => { + it('should show stats for active rule', async () => { + const rule: ThresholdCreateSchema = { + ...getThresholdRuleForSignalTesting(['telemetry']), + threshold: { + field: 'keyword', + value: 1, + }, + }; + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + threshold: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + enabled: 1, + alerts: 4, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + alerts: 4, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show stats for in-active rule', async () => { + const rule: ThresholdCreateSchema = { + ...getThresholdRuleForSignalTesting(['telemetry'], 'rule-1', false), + threshold: { + field: 'keyword', + value: 1, + }, + }; + await createRule(supertest, log, rule); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + threshold: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + disabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + }); + + describe('"ml" rule type', () => { + // Note: We don't actually find signals with this test as we don't have a good way of signal finding with ML rules. + it('should show stats for active rule', async () => { + const rule = getSimpleMlRule('rule-1', true); + await createRule(supertest, log, rule); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + machine_learning: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + enabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show stats for in-active rule', async () => { + const rule = getSimpleMlRule(); + await createRule(supertest, log, rule); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + machine_learning: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + disabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + }); + + describe('"indicator_match/threat_match" rule type', () => { + it('should show stats for active rule', async () => { + const rule: ThreatMatchCreateSchema = { + ...getSimpleThreatMatch('rule-1', true), + index: ['telemetry'], + threat_index: ['telemetry'], + threat_mapping: [ + { + entries: [ + { + field: 'keyword', + value: 'keyword', + type: 'mapping', + }, + ], + }, + ], + }; + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + threat_match: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + enabled: 1, + alerts: 4, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + alerts: 4, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show stats for in-active rule', async () => { + const rule = getSimpleThreatMatch(); + await createRule(supertest, log, rule); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + threat_match: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + disabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + }); + + describe('"pre-packaged" rules', async () => { + it('should show stats for totals for in-active pre-packaged rules', async () => { + await installPrePackagedRules(supertest, log); + await retry.try(async () => { + const stats = await getStats(supertest, log); + expect(stats.detection_rules.detection_rule_usage.elastic_total.enabled).above(0); + expect(stats.detection_rules.detection_rule_usage.elastic_total.disabled).above(0); + expect(stats.detection_rules.detection_rule_usage.elastic_total.enabled).above(0); + expect(stats.detection_rules.detection_rule_usage.custom_total.enabled).equal(0); + expect(stats.detection_rules.detection_rule_detail.length).above(0); + }); + }); + + it('should show stats for the detection_rule_details for pre-packaged rules', async () => { + await installPrePackagedRules(supertest, log); + await retry.try(async () => { + const stats = await getStats(supertest, log); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json + // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id + const foundRule = stats.detection_rules.detection_rule_detail.find( + (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' + ); + if (foundRule == null) { + throw new Error('Found rule should not be null'); + } + const { + created_on: createdOn, + updated_on: updatedOn, + rule_id: ruleId, + ...omittedFields + } = foundRule; + expect(omittedFields).to.eql({ + rule_name: 'Endpoint Security', + rule_type: 'query', + rule_version: 3, + enabled: true, + elastic_rule: true, + alert_count_daily: 0, + cases_count_total: 0, + }); + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts new file mode 100644 index 0000000000000..cf9db6373033a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Detection rule type telemetry', function () { + describe('', function () { + this.tags('ciGroup11'); + loadTestFile(require.resolve('./detection_rules')); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 01e4e85bb7d03..858c4901b2dd3 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -34,6 +34,7 @@ import { EqlCreateSchema, ThresholdCreateSchema, PreviewRulesSchema, + ThreatMatchCreateSchema, } from '../../plugins/security_solution/common/detection_engine/schemas/request'; import { signalsMigrationType } from '../../plugins/security_solution/server/lib/detection_engine/migrations/saved_objects'; import { @@ -53,6 +54,7 @@ import { UPDATE_OR_CREATE_LEGACY_ACTIONS, } from '../../plugins/security_solution/common/constants'; import { RACAlert } from '../../plugins/security_solution/server/lib/detection_engine/rule_types/types'; +import { DetectionMetrics } from '../../plugins/security_solution/server/usage/detections/types'; /** * This will remove server generated properties such as date times, etc... @@ -1827,3 +1829,151 @@ export const getOpenSignals = async ( await refreshIndex(es, '.alerts-security.alerts-default*'); return getSignalsByIds(supertest, log, [rule.id]); }; + +/** + * Cluster stats URL. Replace this with any from kibana core if there is ever a constant there for this. + */ +export const getStatsUrl = (): string => '/api/telemetry/v2/clusters/_stats'; + +/** + * Initial detection metrics initialized. + */ +export const getInitialDetectionMetrics = (): DetectionMetrics => ({ + ml_jobs: { + ml_job_usage: { + custom: { + enabled: 0, + disabled: 0, + }, + elastic: { + enabled: 0, + disabled: 0, + }, + }, + ml_job_metrics: [], + }, + detection_rules: { + detection_rule_detail: [], + detection_rule_usage: { + query: { + enabled: 0, + disabled: 0, + alerts: 0, + cases: 0, + }, + threshold: { + enabled: 0, + disabled: 0, + alerts: 0, + cases: 0, + }, + eql: { + enabled: 0, + disabled: 0, + alerts: 0, + cases: 0, + }, + machine_learning: { + enabled: 0, + disabled: 0, + alerts: 0, + cases: 0, + }, + threat_match: { + enabled: 0, + disabled: 0, + alerts: 0, + cases: 0, + }, + elastic_total: { + enabled: 0, + disabled: 0, + alerts: 0, + cases: 0, + }, + custom_total: { + enabled: 0, + disabled: 0, + alerts: 0, + cases: 0, + }, + }, + }, +}); + +/** + * Given a body this will return the detection metrics from it. + * @param body The Stats body + * @returns Detection metrics + */ +export const getDetectionMetricsFromBody = ( + body: Array<{ + stats: { + stack_stats: { + kibana: { plugins: { security_solution: { detectionMetrics: DetectionMetrics } } }; + }; + }; + }> +): DetectionMetrics => { + return body[0].stats.stack_stats.kibana.plugins.security_solution.detectionMetrics; +}; + +/** + * Gets the stats from the stats endpoint + * @param supertest The supertest agent. + * @returns The detection metrics + */ +export const getStats = async ( + supertest: SuperTest.SuperTest, + log: ToolingLog +): Promise => { + const response = await supertest + .post(getStatsUrl()) + .set('kbn-xsrf', 'true') + .send({ unencrypted: true }); + if (response.status !== 200) { + log.error( + `Did not get an expected 200 "ok" when getting the stats for detections. CI issues could happen. Suspect this line if you are seeing CI issues. body: ${JSON.stringify( + response.body + )}, status: ${JSON.stringify(response.status)}` + ); + } + return getDetectionMetricsFromBody(response.body); +}; + +/** + * This is a typical simple indicator match/threat match for testing that is easy for most basic testing + * @param ruleId + * @param enabled Enables the rule on creation or not. Defaulted to false. + */ +export const getSimpleThreatMatch = ( + ruleId = 'rule-1', + enabled = false +): ThreatMatchCreateSchema => ({ + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + enabled, + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: ruleId, + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: '*:*', + threat_index: ['auditbeat-*'], + threat_mapping: [ + // We match host.name against host.name + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], +}); diff --git a/x-pack/test/functional/es_archives/security_solution/README.md b/x-pack/test/functional/es_archives/security_solution/README.md index c832e0835bbbc..897da48316155 100644 --- a/x-pack/test/functional/es_archives/security_solution/README.md +++ b/x-pack/test/functional/es_archives/security_solution/README.md @@ -9,3 +9,5 @@ or ``` x-pack/test/api_integration/apis/security_solution ``` + +* Folder `telemetry` is for the tests underneath `detection_engine_api_integration/security_and_spaces/tests/telemetry`. diff --git a/x-pack/test/functional/es_archives/security_solution/telemetry/data.json b/x-pack/test/functional/es_archives/security_solution/telemetry/data.json new file mode 100644 index 0000000000000..587b1a496c737 --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/telemetry/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "telemetry", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "keyword": "word one" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "telemetry", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "keyword": "word two" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "telemetry", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "keyword": "word three" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "telemetry", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "keyword": "word four" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/security_solution/telemetry/mappings.json b/x-pack/test/functional/es_archives/security_solution/telemetry/mappings.json new file mode 100644 index 0000000000000..ec48b7b750763 --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/telemetry/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "telemetry", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "keyword": { "type": "keyword" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} From 6c0f7be1641adad587440c042c29082737ab9b3d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 10 Dec 2021 02:45:33 +0000 Subject: [PATCH 011/145] chore(NA): use internal pkg_npm on @kbn/eslint-plugin-eslint (#120940) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-eslint-plugin-eslint/BUILD.bazel | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/kbn-eslint-plugin-eslint/BUILD.bazel b/packages/kbn-eslint-plugin-eslint/BUILD.bazel index 5baab89d6f03d..c02a468456f77 100644 --- a/packages/kbn-eslint-plugin-eslint/BUILD.bazel +++ b/packages/kbn-eslint-plugin-eslint/BUILD.bazel @@ -1,4 +1,5 @@ -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "pkg_npm") PKG_BASE_NAME = "kbn-eslint-plugin-eslint" PKG_REQUIRE_NAME = "@kbn/eslint-plugin-eslint" @@ -28,7 +29,7 @@ NPM_MODULE_EXTRA_FILES = [ "README.md", ] -DEPS = [ +RUNTIME_DEPS = [ "@npm//@babel/eslint-parser", "@npm//dedent", "@npm//eslint", @@ -41,7 +42,7 @@ js_library( srcs = NPM_MODULE_EXTRA_FILES + [ ":srcs", ], - deps = DEPS, + deps = RUNTIME_DEPS, package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) From 9b6995c30d73c4e437b44d901848f7beb4dfa548 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 10 Dec 2021 02:45:49 +0000 Subject: [PATCH 012/145] chore(NA): use internal pkg_npm on @kbn/expect (#120941) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-expect/BUILD.bazel | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kbn-expect/BUILD.bazel b/packages/kbn-expect/BUILD.bazel index b7eb91a451b9a..9f74cfe6a093d 100644 --- a/packages/kbn-expect/BUILD.bazel +++ b/packages/kbn-expect/BUILD.bazel @@ -1,4 +1,5 @@ -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "pkg_npm") PKG_BASE_NAME = "kbn-expect" PKG_REQUIRE_NAME = "@kbn/expect" From b5b4ae73d29d27f30ece9c560741b91c2dd95a9a Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Fri, 10 Dec 2021 08:57:21 +0100 Subject: [PATCH 013/145] fix setup error during test (#120917) --- .../integration_tests/7_13_0_failed_action_tasks.test.ts | 3 +-- .../server/services/epm/elasticsearch/template/install.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/server/saved_objects/migrations/integration_tests/7_13_0_failed_action_tasks.test.ts b/src/core/server/saved_objects/migrations/integration_tests/7_13_0_failed_action_tasks.test.ts index 2def8e375c81f..479b1e78e1b72 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/7_13_0_failed_action_tasks.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/7_13_0_failed_action_tasks.test.ts @@ -19,8 +19,7 @@ async function removeLogFile() { await fs.unlink(logFilePath).catch(() => void 0); } -// FLAKY: https://github.com/elastic/kibana/issues/118626 -describe.skip('migration from 7.13 to 7.14+ with many failed action_tasks', () => { +describe('migration from 7.13 to 7.14+ with many failed action_tasks', () => { let esServer: kbnTestServer.TestElasticsearchUtils; let root: Root; let startES: () => Promise; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index 7031ef1e6a33f..4224ff6b01a19 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -341,7 +341,7 @@ export async function ensureDefaultComponentTemplate( await putComponentTemplate(esClient, logger, { name: FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME, body: FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT, - }); + }).clusterPromise; } return { isCreated: !existingTemplate }; From 7ceb878989a09b0c364a79d5ad834e944bf3aab6 Mon Sep 17 00:00:00 2001 From: Muhammad Ibragimov <53621505+mibragimov@users.noreply.github.com> Date: Fri, 10 Dec 2021 14:36:04 +0500 Subject: [PATCH 014/145] [Console] Highlight the tutorial example text with console syntax (#120474) * Highlight tutorial example text Co-authored-by: Muhammad Ibragimov --- .../application/components/editor_example.tsx | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/plugins/console/public/application/components/editor_example.tsx b/src/plugins/console/public/application/components/editor_example.tsx index 577f32fa912fb..21e3ab0c7d274 100644 --- a/src/plugins/console/public/application/components/editor_example.tsx +++ b/src/plugins/console/public/application/components/editor_example.tsx @@ -8,8 +8,10 @@ import { EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useEffect } from 'react'; -import { createReadOnlyAceEditor } from '../models/legacy_core_editor'; +import React, { useEffect, useRef } from 'react'; +import { createReadOnlyAceEditor, CustomAceEditor } from '../models/sense_editor'; +// @ts-ignore +import { Mode } from '../models/legacy_core_editor/mode/input'; interface EditorExampleProps { panel: string; @@ -27,21 +29,33 @@ GET index/_doc/1 `; export function EditorExample(props: EditorExampleProps) { - const elemId = `help-example-${props.panel}`; const inputId = `help-example-${props.panel}-input`; + const wrapperDivRef = useRef(null); + const editorRef = useRef(); useEffect(() => { - const el = document.getElementById(elemId)!; - el.textContent = exampleText.trim(); - const editor = createReadOnlyAceEditor(el); - const textarea = el.querySelector('textarea')!; - textarea.setAttribute('id', inputId); - textarea.setAttribute('readonly', 'true'); + if (wrapperDivRef.current) { + editorRef.current = createReadOnlyAceEditor(wrapperDivRef.current); + + const editor = editorRef.current; + editor.update(exampleText.trim()); + editor.session.setMode(new Mode()); + editor.session.setUseWorker(false); + editor.setHighlightActiveLine(false); + + const textareaElement = wrapperDivRef.current.querySelector('textarea'); + if (textareaElement) { + textareaElement.setAttribute('id', inputId); + textareaElement.setAttribute('readonly', 'true'); + } + } return () => { - editor.destroy(); + if (editorRef.current) { + editorRef.current.destroy(); + } }; - }, [elemId, inputId]); + }, [inputId]); return ( <> @@ -52,7 +66,7 @@ export function EditorExample(props: EditorExampleProps) { })} -
+
); } From e60c6e1873e25b2f80433d7d4b134212af5b8646 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 10 Dec 2021 10:52:15 +0100 Subject: [PATCH 015/145] [home] use correct cloud reset link for tutorials (#120884) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../home/server/tutorials/instructions/cloud_instructions.ts | 3 ++- x-pack/plugins/cloud/public/plugin.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts b/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts index ed9e588a999b4..6d547b2a1d40d 100644 --- a/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts @@ -7,13 +7,14 @@ */ import { i18n } from '@kbn/i18n'; + export const cloudPasswordAndResetLink = i18n.translate( 'home.tutorials.common.cloudInstructions.passwordAndResetLink', { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.' + `\\{#config.cloud.profileUrl\\} - Forgot the password? [Reset in Elastic Cloud](\\{config.cloud.baseUrl\\}\\{config.cloud.profileUrl\\}). + Forgot the password? [Reset in Elastic Cloud](\\{config.cloud.baseUrl\\}\\{config.cloud.deploymentUrl\\}/security). \\{/config.cloud.profileUrl\\}`, values: { passwordTemplate: '``' }, } diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index e71b145c438ed..81aad8bf79ccc 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -98,7 +98,7 @@ export class CloudPlugin implements Plugin { if (home) { home.environment.update({ cloud: this.isCloudEnabled }); if (this.isCloudEnabled) { - home.tutorials.setVariable('cloud', { id, baseUrl, profileUrl }); + home.tutorials.setVariable('cloud', { id, baseUrl, profileUrl, deploymentUrl }); } } From 0b200103aa0e950b27af3336bc40ecc250d61613 Mon Sep 17 00:00:00 2001 From: Jan Monschke Date: Fri, 10 Dec 2021 11:30:01 +0100 Subject: [PATCH 016/145] [Security Solution][Investigations] Alert flyout UX updates (pt. 1) (#120347) * feat: Move timestamp from summary to below the title * refactor: creat reusable getEnrichedFieldInfo This method can be used in different places to enrich the field data. * feat: make unpadded/unsized version of ActionCell available Ideally, ActionCell and HoverActions would not have padding and width declaration. This could be part of a future refactor. For now, a version with padding and size information is all that is needed. * feat: add OverviewCards w/ severity, risk score and rule name * feat: add status to overview cards * refactor: use FormattedFieldValue instead of RuleStatus directly * fix: limit height of the overview cards * fix: clamp content to 2 lines * chore: add displayName * feat: Add interactive popover to status badge * chore: remove signal status from document summary * feat: Remove rule link and headline from reason component * feat: Add table-tab pivot link * feat: close alert flyout after status change * test: fix snapshots * chore: remove unused imports * chore: use correct padding in context menu * chore: split over cards into multiple files * chore: use shared severity badge * chore: revert back to plain risk score text * chore: rename and move overview * fix: fix alignment between actions and content * fix: fix types in test * chore: remove unused import * chore: useMemo & useCallback * chore: import type * feat: add iconType, iconSide and onClickArialabel to rule status * feat: add hover actions to the alert status overview card * fix: use correct data * fix: action cell did not look good on small screens Now the action cell slides in similar to how the action buttons slide in in a data grid. * fix: use different card layout based on container width * fix: use new Severity type * fix: align children centered * test: add popover button tests * test: add overview card test * test: test overview cards * fix: prevent rendering of two cards in two ingle rows * fix: change i18n key to prevent a duplicate key * chore: remove unused translations * nit: use less vertical screen estate Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../event_details/alert_summary_view.test.tsx | 2 + .../event_details/alert_summary_view.tsx | 17 +- .../event_details/event_details.test.tsx | 2 + .../event_details/event_details.tsx | 28 +- .../event_details/get_alert_summary_rows.tsx | 83 ++--- .../components/event_details/helpers.tsx | 55 +++- .../__snapshots__/index.test.tsx.snap | 311 ++++++++++++++++++ .../event_details/overview/index.test.tsx | 198 +++++++++++ .../event_details/overview/index.tsx | 210 ++++++++++++ .../overview/overview_card.test.tsx | 71 ++++ .../event_details/overview/overview_card.tsx | 99 ++++++ .../overview/status_popover_button.test.tsx | 82 +++++ .../overview/status_popover_button.tsx | 81 +++++ .../components/event_details/reason.tsx | 57 +--- .../components/event_details/summary_view.tsx | 41 ++- .../event_details/table/action_cell.tsx | 14 +- .../components/event_details/translations.ts | 7 + .../common/components/event_details/types.ts | 19 ++ .../common/components/hover_actions/index.tsx | 17 +- .../components/alerts_table/translations.ts | 21 +- .../__snapshots__/index.test.tsx.snap | 5 + .../event_details/expandable_event.tsx | 25 +- .../side_panel/event_details/index.tsx | 14 +- .../body/renderers/formatted_field.tsx | 6 + .../timeline/body/renderers/rule_status.tsx | 27 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 27 files changed, 1321 insertions(+), 173 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx index 7e1e71a01642f..c397ac313c48c 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx @@ -30,6 +30,8 @@ const props = { browserFields: mockBrowserFields, eventId: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31', timelineId: 'detections-page', + title: '', + goToTable: jest.fn(), }; describe('AlertSummaryView', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx index b42a0425355cc..c30837dc6eca8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiBasicTableColumn, EuiSpacer } from '@elastic/eui'; +import { EuiBasicTableColumn } from '@elastic/eui'; import React, { useMemo } from 'react'; import { BrowserFields } from '../../../../common/search_strategy/index_fields'; @@ -60,18 +60,21 @@ const AlertSummaryViewComponent: React.FC<{ eventId: string; isDraggable?: boolean; timelineId: string; - title?: string; -}> = ({ browserFields, data, eventId, isDraggable, timelineId, title }) => { + title: string; + goToTable: () => void; +}> = ({ browserFields, data, eventId, isDraggable, timelineId, title, goToTable }) => { const summaryRows = useMemo( () => getSummaryRows({ browserFields, data, eventId, isDraggable, timelineId }), [browserFields, data, eventId, isDraggable, timelineId] ); return ( - <> - - - + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index 37ca3b0b897a6..14910c77d198c 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -48,6 +48,8 @@ describe('EventDetails', () => { timelineId: 'test', eventView: EventsViewType.summaryView, hostRisk: { fields: [], loading: true }, + indexName: 'test', + handleOnEventClosed: jest.fn(), rawEventData, }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 0fe48d5a998ea..08f97ab7d1bc7 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -6,6 +6,7 @@ */ import { + EuiHorizontalRule, EuiTabbedContent, EuiTabbedContentTab, EuiSpacer, @@ -39,7 +40,9 @@ import { EnrichmentRangePicker } from './cti_details/enrichment_range_picker'; import { Reason } from './reason'; import { InvestigationGuideView } from './investigation_guide_view'; + import { HostRisk } from '../../containers/hosts_risk/use_hosts_risk_score'; +import { Overview } from './overview'; type EventViewTab = EuiTabbedContentTab; @@ -59,12 +62,14 @@ interface Props { browserFields: BrowserFields; data: TimelineEventsDetailsItem[]; id: string; + indexName: string; isAlert: boolean; isDraggable?: boolean; rawEventData: object | undefined; timelineTabType: TimelineTabs | 'flyout'; timelineId: string; hostRisk: HostRisk | null; + handleOnEventClosed: () => void; } export const Indent = styled.div` @@ -105,18 +110,21 @@ const EventDetailsComponent: React.FC = ({ browserFields, data, id, + indexName, isAlert, isDraggable, rawEventData, timelineId, timelineTabType, hostRisk, + handleOnEventClosed, }) => { const [selectedTabId, setSelectedTabId] = useState(EventsViewType.summaryView); const handleTabClick = useCallback( (tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EventViewId), - [setSelectedTabId] + [] ); + const goToTableTab = useCallback(() => setSelectedTabId(EventsViewType.tableView), []); const eventFields = useMemo(() => getEnrichmentFields(data), [data]); const existingEnrichments = useMemo( @@ -152,7 +160,19 @@ const EventDetailsComponent: React.FC = ({ name: i18n.OVERVIEW, content: ( <> + + + + = ({ timelineId, title: i18n.DUCOMENT_SUMMARY, }} + goToTable={goToTableTab} /> {(enrichmentCount > 0 || hostRisk) && ( @@ -188,8 +209,9 @@ const EventDetailsComponent: React.FC = ({ } : undefined, [ - isAlert, id, + indexName, + isAlert, data, browserFields, isDraggable, @@ -198,6 +220,8 @@ const EventDetailsComponent: React.FC = ({ allEnrichments, isEnrichmentsLoading, hostRisk, + goToTableTab, + handleOnEventClosed, ] ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx index 4af444c2ab8ad..0bf404fe51e39 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx @@ -5,53 +5,31 @@ * 2.0. */ -import { get, getOr, find, isEmpty } from 'lodash/fp'; +import { getOr, find, isEmpty } from 'lodash/fp'; import * as i18n from './translations'; import { BrowserFields } from '../../../../common/search_strategy/index_fields'; import { - ALERTS_HEADERS_RISK_SCORE, - ALERTS_HEADERS_RULE, - ALERTS_HEADERS_SEVERITY, ALERTS_HEADERS_THRESHOLD_CARDINALITY, ALERTS_HEADERS_THRESHOLD_COUNT, ALERTS_HEADERS_THRESHOLD_TERMS, ALERTS_HEADERS_RULE_NAME, - SIGNAL_STATUS, ALERTS_HEADERS_TARGET_IMPORT_HASH, - TIMESTAMP, ALERTS_HEADERS_RULE_DESCRIPTION, } from '../../../detections/components/alerts_table/translations'; import { AGENT_STATUS_FIELD_NAME, IP_FIELD_TYPE, - SIGNAL_RULE_NAME_FIELD_NAME, } from '../../../timelines/components/timeline/body/renderers/constants'; import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../../../network/components/ip'; -import { SummaryRow } from './helpers'; +import { getEnrichedFieldInfo, SummaryRow } from './helpers'; +import { EventSummaryField } from './types'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; import { isAlertFromEndpointEvent } from '../../utils/endpoint_alert_check'; import { EventCode } from '../../../../common/ecs/event'; -interface EventSummaryField { - id: string; - label?: string; - linkField?: string; - fieldType?: string; - overrideField?: string; -} - const defaultDisplayFields: EventSummaryField[] = [ - { id: 'kibana.alert.workflow_status', label: SIGNAL_STATUS }, - { id: '@timestamp', label: TIMESTAMP }, - { - id: SIGNAL_RULE_NAME_FIELD_NAME, - linkField: 'kibana.alert.rule.uuid', - label: ALERTS_HEADERS_RULE, - }, - { id: 'kibana.alert.rule.severity', label: ALERTS_HEADERS_SEVERITY }, - { id: 'kibana.alert.rule.risk_score', label: ALERTS_HEADERS_RISK_SCORE }, { id: 'host.name' }, { id: 'agent.id', overrideField: AGENT_STATUS_FIELD_NAME, label: i18n.AGENT_STATUS }, { id: 'user.name' }, @@ -151,50 +129,34 @@ export const getSummaryRows = ({ const tableFields = getEventFieldsToDisplay({ eventCategory, eventCode }); return data != null - ? tableFields.reduce((acc, item) => { - const initialDescription = { - contextId: timelineId, - eventId, - isDraggable, - value: null, - fieldType: 'string', - linkValue: undefined, - timelineId, - }; - const field = data.find((d) => d.field === item.id); - if (!field || isEmpty(field?.values)) { + ? tableFields.reduce((acc, field) => { + const item = data.find((d) => d.field === field.id); + if (!item || isEmpty(item?.values)) { return acc; } const linkValueField = - item.linkField != null && data.find((d) => d.field === item.linkField); - const linkValue = getOr(null, 'originalValue.0', linkValueField); - const value = getOr(null, 'originalValue.0', field); - const category = field.category ?? ''; - const fieldName = field.field ?? ''; - - const browserField = get([category, 'fields', fieldName], browserFields); + field.linkField != null && data.find((d) => d.field === field.linkField); const description = { - ...initialDescription, - data: { - field: field.field, - format: browserField?.format ?? '', - type: browserField?.type ?? '', - isObjectArray: field.isObjectArray, - ...(item.overrideField ? { field: item.overrideField } : {}), - }, - values: field.values, - linkValue: linkValue ?? undefined, - fieldFromBrowserField: browserField, + ...getEnrichedFieldInfo({ + item, + linkValueField: linkValueField || undefined, + contextId: timelineId, + timelineId, + browserFields, + eventId, + field, + }), + isDraggable, }; - if (item.id === 'agent.id' && !isAlertFromEndpointEvent({ data })) { + if (field.id === 'agent.id' && !isAlertFromEndpointEvent({ data })) { return acc; } - if (item.id === 'kibana.alert.threshold_result.terms') { + if (field.id === 'kibana.alert.threshold_result.terms') { try { - const terms = getOr(null, 'originalValue', field); + const terms = getOr(null, 'originalValue', item); const parsedValue = terms.map((term: string) => JSON.parse(term)); const thresholdTerms = (parsedValue ?? []).map( (entry: { field: string; value: string }) => { @@ -213,8 +175,9 @@ export const getSummaryRows = ({ } } - if (item.id === 'kibana.alert.threshold_result.cardinality') { + if (field.id === 'kibana.alert.threshold_result.cardinality') { try { + const value = getOr(null, 'originalValue.0', field); const parsedValue = JSON.parse(value); return [ ...acc, @@ -234,7 +197,7 @@ export const getSummaryRows = ({ return [ ...acc, { - title: item.label ?? item.id, + title: field.label ?? field.id, description, }, ]; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx index 648bc96b5c9e7..dcca42f2a1df7 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx @@ -22,7 +22,8 @@ import { DEFAULT_DATE_COLUMN_MIN_WIDTH, DEFAULT_COLUMN_MIN_WIDTH, } from '../../../timelines/components/timeline/body/constants'; -import { FieldsData } from './types'; +import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; +import type { EnrichedFieldInfo, EventSummaryField } from './types'; import * as i18n from './translations'; import { ColumnHeaderOptions } from '../../../../common/types'; @@ -56,14 +57,8 @@ export interface Item { export interface AlertSummaryRow { title: string; - description: { - data: FieldsData; - eventId: string; + description: EnrichedFieldInfo & { isDraggable?: boolean; - fieldFromBrowserField?: BrowserField; - linkValue: string | undefined; - timelineId: string; - values: string[] | null | undefined; }; } @@ -232,3 +227,47 @@ export const getSummaryColumns = ( }, ]; }; + +export function getEnrichedFieldInfo({ + browserFields, + contextId, + eventId, + field, + item, + linkValueField, + timelineId, +}: { + browserFields: BrowserFields; + contextId: string; + item: TimelineEventsDetailsItem; + eventId: string; + field?: EventSummaryField; + timelineId: string; + linkValueField?: TimelineEventsDetailsItem; +}): EnrichedFieldInfo { + const fieldInfo = { + contextId, + eventId, + fieldType: 'string', + linkValue: undefined, + timelineId, + }; + const linkValue = getOr(null, 'originalValue.0', linkValueField); + const category = item.category ?? ''; + const fieldName = item.field ?? ''; + + const browserField = get([category, 'fields', fieldName], browserFields); + const overrideField = field?.overrideField; + return { + ...fieldInfo, + data: { + field: overrideField ?? fieldName, + format: browserField?.format ?? '', + type: browserField?.type ?? '', + isObjectArray: item.isObjectArray, + }, + values: item.values, + linkValue: linkValue ?? undefined, + fieldFromBrowserField: browserField, + }; +} diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..4e62766fc1477 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap @@ -0,0 +1,311 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = ` + + .c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c6:focus-within .timelines__hoverActionButton, +.c6:focus-within .securitySolution__hoverActionButton { + opacity: 1; +} + +.c6:hover .timelines__hoverActionButton, +.c6:hover .securitySolution__hoverActionButton { + opacity: 1; +} + +.c6 .timelines__hoverActionButton, +.c6 .securitySolution__hoverActionButton { + opacity: 0; +} + +.c6 .timelines__hoverActionButton:focus, +.c6 .securitySolution__hoverActionButton:focus { + opacity: 1; +} + +.c3 { + text-transform: capitalize; +} + +.c5 { + width: 0; + -webkit-transform: translate(6px); + -ms-transform: translate(6px); + transform: translate(6px); + -webkit-transition: -webkit-transform 50ms ease-in-out; + -webkit-transition: transform 50ms ease-in-out; + transition: transform 50ms ease-in-out; + margin-left: 8px; +} + +.c1.c1.c1 { + background-color: #25262e; + padding: 8px; + height: 78px; +} + +.c1 .hoverActions-active .timelines__hoverActionButton, +.c1 .hoverActions-active .securitySolution__hoverActionButton { + opacity: 1; +} + +.c1:hover .timelines__hoverActionButton, +.c1:hover .securitySolution__hoverActionButton { + opacity: 1; +} + +.c1:hover .c4 { + width: auto; + -webkit-transform: translate(0); + -ms-transform: translate(0); + transform: translate(0); +} + +.c2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.c0 { + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; +} + +
+
+
+
+
+ Status +
+
+
+
+
+
+ +
+
+
+
+
+
+

+ You are in a dialog, containing options for field kibana.alert.workflow_status. Press tab to navigate options. Press escape to exit. +

+
+ Filter button +
+
+ Filter out button +
+
+ Overflow button +
+
+
+
+
+
+
+
+
+
+ Risk Score +
+
+
+
+ 47 +
+
+
+
+

+ You are in a dialog, containing options for field kibana.alert.rule.risk_score. Press tab to navigate options. Press escape to exit. +

+
+ Filter button +
+
+ Filter out button +
+
+ Overflow button +
+
+
+
+
+
+
+
+
+
+
+
+
+ Rule +
+
+
+
+ +
+
+
+
+

+ You are in a dialog, containing options for field kibana.alert.rule.name. Press tab to navigate options. Press escape to exit. +

+
+ Filter button +
+
+ Filter out button +
+
+ Overflow button +
+
+
+
+
+
+
+
+
+ +`; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx new file mode 100644 index 0000000000000..50da80f7b1304 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { Overview } from './'; +import { TestProviders } from '../../../../common/mock'; + +jest.mock('../../../lib/kibana'); +jest.mock('../../utils', () => ({ + useThrottledResizeObserver: () => ({ width: 400 }), // force row-chunking +})); + +describe('Event Details Overview Cards', () => { + it('renders all cards', () => { + const { getByText } = render( + + + + ); + + getByText('Status'); + getByText('Severity'); + getByText('Risk Score'); + getByText('Rule'); + }); + + it('renders all cards it has data for', () => { + const { getByText, queryByText } = render( + + + + ); + + getByText('Status'); + getByText('Risk Score'); + getByText('Rule'); + + expect(queryByText('Severity')).not.toBeInTheDocument(); + }); + + it('renders rows and spacers correctly', () => { + const { asFragment } = render( + + + + ); + + expect(asFragment()).toMatchSnapshot(); + }); +}); + +const props = { + handleOnEventClosed: jest.fn(), + contextId: 'detections-page', + eventId: 'testId', + indexName: 'testIndex', + timelineId: 'page', + data: [ + { + category: 'kibana', + field: 'kibana.alert.rule.risk_score', + values: ['47'], + originalValue: ['47'], + isObjectArray: false, + }, + { + category: 'kibana', + field: 'kibana.alert.rule.uuid', + values: ['d9f537c0-47b2-11ec-9517-c1c68c44dec0'], + originalValue: ['d9f537c0-47b2-11ec-9517-c1c68c44dec0'], + isObjectArray: false, + }, + { + category: 'kibana', + field: 'kibana.alert.workflow_status', + values: ['open'], + originalValue: ['open'], + isObjectArray: false, + }, + { + category: 'kibana', + field: 'kibana.alert.rule.name', + values: ['More than one event with user name'], + originalValue: ['More than one event with user name'], + isObjectArray: false, + }, + { + category: 'kibana', + field: 'kibana.alert.rule.severity', + values: ['medium'], + originalValue: ['medium'], + isObjectArray: false, + }, + ], + browserFields: { + kibana: { + fields: { + 'kibana.alert.rule.severity': { + category: 'kibana', + count: 0, + name: 'kibana.alert.rule.severity', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: 'string', + shortDotsEnable: false, + isMapped: true, + indexes: ['apm-*-transaction*'], + }, + 'kibana.alert.rule.risk_score': { + category: 'kibana', + count: 0, + name: 'kibana.alert.rule.risk_score', + type: 'number', + esTypes: ['float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: 'number', + shortDotsEnable: false, + isMapped: true, + indexes: ['apm-*-transaction*'], + }, + 'kibana.alert.rule.uuid': { + category: 'kibana', + count: 0, + name: 'kibana.alert.rule.uuid', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: 'string', + shortDotsEnable: false, + isMapped: true, + indexes: ['apm-*-transaction*'], + }, + 'kibana.alert.workflow_status': { + category: 'kibana', + count: 0, + name: 'kibana.alert.workflow_status', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: 'string', + shortDotsEnable: false, + isMapped: true, + indexes: ['apm-*-transaction*'], + }, + 'kibana.alert.rule.name': { + category: 'kibana', + count: 0, + name: 'kibana.alert.rule.name', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: 'string', + shortDotsEnable: false, + isMapped: true, + indexes: ['apm-*-transaction*'], + }, + }, + }, + }, +}; + +const dataWithoutSeverity = props.data.filter( + (data) => data.field !== 'kibana.alert.rule.severity' +); + +const fieldsWithoutSeverity = { + 'kibana.alert.rule.risk_score': props.browserFields.kibana.fields['kibana.alert.rule.risk_score'], + 'kibana.alert.rule.uuid': props.browserFields.kibana.fields['kibana.alert.rule.uuid'], + 'kibana.alert.workflow_status': props.browserFields.kibana.fields['kibana.alert.workflow_status'], + 'kibana.alert.rule.name': props.browserFields.kibana.fields['kibana.alert.rule.name'], +}; + +const propsWithoutSeverity = { + ...props, + browserFields: { kibana: { fields: fieldsWithoutSeverity } }, + data: dataWithoutSeverity, +}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx new file mode 100644 index 0000000000000..70a8ec7ad0d22 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx @@ -0,0 +1,210 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { chunk, find } from 'lodash/fp'; +import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; + +import type { BrowserFields } from '../../../containers/source'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; +import type { EnrichedFieldInfo, EnrichedFieldInfoWithValues } from '../types'; +import { getEnrichedFieldInfo } from '../helpers'; +import { + ALERTS_HEADERS_RISK_SCORE, + ALERTS_HEADERS_RULE, + ALERTS_HEADERS_SEVERITY, + SIGNAL_STATUS, +} from '../../../../detections/components/alerts_table/translations'; +import { + SIGNAL_RULE_NAME_FIELD_NAME, + SIGNAL_STATUS_FIELD_NAME, +} from '../../../../timelines/components/timeline/body/renderers/constants'; +import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; +import { OverviewCardWithActions } from '../overview/overview_card'; +import { StatusPopoverButton } from '../overview/status_popover_button'; +import { SeverityBadge } from '../../../../../public/detections/components/rules/severity_badge'; +import { useThrottledResizeObserver } from '../../utils'; +import { isNotNull } from '../../../../../public/timelines/store/timeline/helpers'; + +export const NotGrowingFlexGroup = euiStyled(EuiFlexGroup)` + flex-grow: 0; +`; + +interface Props { + browserFields: BrowserFields; + contextId: string; + data: TimelineEventsDetailsItem[]; + eventId: string; + handleOnEventClosed: () => void; + indexName: string; + timelineId: string; +} + +export const Overview = React.memo( + ({ browserFields, contextId, data, eventId, handleOnEventClosed, indexName, timelineId }) => { + const statusData = useMemo(() => { + const item = find({ field: SIGNAL_STATUS_FIELD_NAME, category: 'kibana' }, data); + return ( + item && + getEnrichedFieldInfo({ + eventId, + contextId, + timelineId, + browserFields, + item, + }) + ); + }, [browserFields, contextId, data, eventId, timelineId]); + + const severityData = useMemo(() => { + const item = find({ field: 'kibana.alert.rule.severity', category: 'kibana' }, data); + return ( + item && + getEnrichedFieldInfo({ + eventId, + contextId, + timelineId, + browserFields, + item, + }) + ); + }, [browserFields, contextId, data, eventId, timelineId]); + + const riskScoreData = useMemo(() => { + const item = find({ field: 'kibana.alert.rule.risk_score', category: 'kibana' }, data); + return ( + item && + getEnrichedFieldInfo({ + eventId, + contextId, + timelineId, + browserFields, + item, + }) + ); + }, [browserFields, contextId, data, eventId, timelineId]); + + const ruleNameData = useMemo(() => { + const item = find({ field: SIGNAL_RULE_NAME_FIELD_NAME, category: 'kibana' }, data); + const linkValueField = find({ field: 'kibana.alert.rule.uuid', category: 'kibana' }, data); + return ( + item && + getEnrichedFieldInfo({ + eventId, + contextId, + timelineId, + browserFields, + item, + linkValueField, + }) + ); + }, [browserFields, contextId, data, eventId, timelineId]); + + const signalCard = hasData(statusData) ? ( + + + + + + ) : null; + + const severityCard = hasData(severityData) ? ( + + + + + + ) : null; + + const riskScoreCard = hasData(riskScoreData) ? ( + + + {riskScoreData.values[0]} + + + ) : null; + + const ruleNameCard = hasData(ruleNameData) ? ( + + + + + + ) : null; + + const { width, ref } = useThrottledResizeObserver(); + + // 675px is the container width at which none of the cards, when hovered, + // creates a visual overflow in a single row setup + const showAsSingleRow = width === 0 || width >= 675; + + // Only render cards with content + const cards = [signalCard, severityCard, riskScoreCard, ruleNameCard].filter(isNotNull); + + // If there is enough space, render a single row. + // Otherwise, render two rows with each two cards. + const content = showAsSingleRow ? ( + {cards} + ) : ( + <> + {chunk(2, cards).map((elements, index, { length }) => { + // Add a spacer between rows but not after the last row + const addSpacer = index < length - 1; + return ( + <> + {elements} + {addSpacer && } + + ); + })} + + ); + + return
{content}
; + } +); + +function hasData(fieldInfo?: EnrichedFieldInfo): fieldInfo is EnrichedFieldInfoWithValues { + return !!fieldInfo && Array.isArray(fieldInfo.values); +} + +Overview.displayName = 'Overview'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx new file mode 100644 index 0000000000000..8ed3dc7e36165 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { OverviewCardWithActions } from './overview_card'; +import { TestProviders } from '../../../../common/mock'; +import { SeverityBadge } from '../../../../../public/detections/components/rules/severity_badge'; + +const props = { + title: 'Severity', + contextId: 'timeline-case', + enrichedFieldInfo: { + contextId: 'timeline-case', + eventId: 'testid', + fieldType: 'string', + timelineId: 'timeline-case', + data: { + field: 'kibana.alert.rule.severity', + format: 'string', + type: 'string', + isObjectArray: false, + }, + values: ['medium'], + fieldFromBrowserField: { + category: 'kibana', + count: 0, + name: 'kibana.alert.rule.severity', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: 'string', + shortDotsEnable: false, + isMapped: true, + indexes: ['apm-*-transaction*'], + description: '', + example: '', + fields: {}, + }, + }, +}; + +jest.mock('../../../lib/kibana'); + +describe('OverviewCardWithActions', () => { + test('it renders correctly', () => { + const { getByText } = render( + + + + + + ); + + // Headline + getByText('Severity'); + + // Content + getByText('Medium'); + + // Hover actions + getByText('Add To Timeline'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx new file mode 100644 index 0000000000000..4d3dae271f5c9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx @@ -0,0 +1,99 @@ +/* + * 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 { EuiFlexGroup, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import React from 'react'; + +import { ActionCell } from '../table/action_cell'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { EnrichedFieldInfo } from '../types'; + +const ActionWrapper = euiStyled.div` + width: 0; + transform: translate(6px); + transition: transform 50ms ease-in-out; + margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; +`; + +const OverviewPanel = euiStyled(EuiPanel)` + &&& { + background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; + padding: ${({ theme }) => theme.eui.paddingSizes.s}; + height: 78px; + } + + & { + .hoverActions-active { + .timelines__hoverActionButton, + .securitySolution__hoverActionButton { + opacity: 1; + } + } + + &:hover { + .timelines__hoverActionButton, + .securitySolution__hoverActionButton { + opacity: 1; + } + + ${ActionWrapper} { + width: auto; + transform: translate(0); + } + } + } +`; + +interface OverviewCardProps { + title: string; +} + +export const OverviewCard: React.FC = ({ title, children }) => ( + + {title} + + {children} + +); + +OverviewCard.displayName = 'OverviewCard'; + +const ClampedContent = euiStyled.div` + /* Clamp text content to 2 lines */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +`; + +ClampedContent.displayName = 'ClampedContent'; + +type OverviewCardWithActionsProps = OverviewCardProps & { + contextId: string; + enrichedFieldInfo: EnrichedFieldInfo; +}; + +export const OverviewCardWithActions: React.FC = ({ + title, + children, + contextId, + enrichedFieldInfo, +}) => { + return ( + + + {children} + + + + + + + ); +}; + +OverviewCardWithActions.displayName = 'OverviewCardWithActions'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.test.tsx new file mode 100644 index 0000000000000..3c3316618a72c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.test.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { StatusPopoverButton } from './status_popover_button'; +import { TestProviders } from '../../../../common/mock'; + +const props = { + eventId: 'testid', + contextId: 'detections-page', + enrichedFieldInfo: { + contextId: 'detections-page', + eventId: 'testid', + fieldType: 'string', + timelineId: 'detections-page', + data: { + field: 'kibana.alert.workflow_status', + format: 'string', + type: 'string', + isObjectArray: false, + }, + values: ['open'], + fieldFromBrowserField: { + category: 'kibana', + count: 0, + name: 'kibana.alert.workflow_status', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: 'string', + shortDotsEnable: false, + isMapped: true, + indexes: ['apm-*-transaction*'], + description: '', + example: '', + fields: {}, + }, + }, + indexName: '.internal.alerts-security.alerts-default-000001', + timelineId: 'detections-page', + handleOnEventClosed: jest.fn(), +}; + +jest.mock( + '../../../../detections/containers/detection_engine/alerts/use_alerts_privileges', + () => ({ + useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }), + }) +); + +describe('StatusPopoverButton', () => { + test('it renders the correct status', () => { + const { getByText } = render( + + + + ); + + getByText('open'); + }); + + test('it shows the correct options when clicked', () => { + const { getByText } = render( + + + + ); + + getByText('open').click(); + + getByText('Mark as acknowledged'); + getByText('Mark as closed'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.tsx new file mode 100644 index 0000000000000..0ffa1570e7c29 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiContextMenuPanel, EuiPopover, EuiPopoverTitle } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; + +import { useAlertsActions } from '../../../../detections/components/alerts_table/timeline_actions/use_alerts_actions'; +import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { + CHANGE_ALERT_STATUS, + CLICK_TO_CHANGE_ALERT_STATUS, +} from '../../../../detections/components/alerts_table/translations'; +import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; +import type { EnrichedFieldInfoWithValues } from '../types'; + +interface StatusPopoverButtonProps { + eventId: string; + contextId: string; + enrichedFieldInfo: EnrichedFieldInfoWithValues; + indexName: string; + timelineId: string; + handleOnEventClosed: () => void; +} + +export const StatusPopoverButton = React.memo( + ({ eventId, contextId, enrichedFieldInfo, indexName, timelineId, handleOnEventClosed }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + const closeAfterAction = useCallback(() => { + closePopover(); + handleOnEventClosed(); + }, [closePopover, handleOnEventClosed]); + + const { actionItems } = useAlertsActions({ + closePopover: closeAfterAction, + eventId, + timelineId, + indexName, + alertStatus: enrichedFieldInfo.values[0] as Status, + }); + + const button = useMemo( + () => ( + + ), + [contextId, eventId, enrichedFieldInfo, togglePopover] + ); + + return ( + + {CHANGE_ALERT_STATUS} + + + ); + } +); + +StatusPopoverButton.displayName = 'StatusPopoverButton'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/reason.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/reason.tsx index d06f4d3ea105b..88208dd1b9780 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/reason.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/reason.tsx @@ -5,18 +5,12 @@ * 2.0. */ -import { EuiTextColor, EuiFlexItem, EuiSpacer, EuiHorizontalRule, EuiTitle } from '@elastic/eui'; -import { ALERT_REASON, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; +import { EuiTextColor, EuiFlexItem } from '@elastic/eui'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import React, { useMemo } from 'react'; -import styled from 'styled-components'; -import { getRuleDetailsUrl, useFormatUrl } from '../link_to'; -import * as i18n from './translations'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; -import { LinkAnchor } from '../links'; -import { useKibana } from '../../lib/kibana'; -import { APP_UI_ID, SecurityPageName } from '../../../../common/constants'; import { EVENT_DETAILS_PLACEHOLDER } from '../../../timelines/components/side_panel/event_details/translations'; import { getFieldValue } from '../../../detections/components/host_isolation/helpers'; @@ -25,16 +19,7 @@ interface Props { eventId: string; } -export const Indent = styled.div` - padding: 0 8px; - word-break: break-word; - line-height: 1.7em; -`; - export const ReasonComponent: React.FC = ({ eventId, data }) => { - const { navigateToApp } = useKibana().services.application; - const { formatUrl } = useFormatUrl(SecurityPageName.rules); - const reason = useMemo(() => { const siemSignalsReason = getFieldValue( { category: 'signal', field: 'signal.alert.reason' }, @@ -44,47 +29,11 @@ export const ReasonComponent: React.FC = ({ eventId, data }) => { return aadReason.length > 0 ? aadReason : siemSignalsReason; }, [data]); - const ruleId = useMemo(() => { - const siemSignalsRuleId = getFieldValue({ category: 'signal', field: 'signal.rule.id' }, data); - const aadRuleId = getFieldValue({ category: 'kibana', field: ALERT_RULE_UUID }, data); - return aadRuleId.length > 0 ? aadRuleId : siemSignalsRuleId; - }, [data]); - if (!eventId) { return {EVENT_DETAILS_PLACEHOLDER}; } - return reason ? ( - - - -
{i18n.REASON}
-
- - - {reason} - - - - - void }) => { - ev.preventDefault(); - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(ruleId), - }); - }} - href={formatUrl(getRuleDetailsUrl(ruleId))} - > - {i18n.VIEW_RULE_DETAIL_PAGE} - - - - -
- ) : null; + return reason ? {reason} : null; }; ReasonComponent.displayName = 'ReasonComponent'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx index cf8bf3ddb7474..a84d831524983 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx @@ -5,14 +5,24 @@ * 2.0. */ -import { EuiInMemoryTable, EuiBasicTableColumn, EuiTitle } from '@elastic/eui'; +import { + EuiInMemoryTable, + EuiBasicTableColumn, + EuiLink, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, +} from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; import { SummaryRow } from './helpers'; +import { VIEW_ALL_DOCUMENT_FIELDS } from './translations'; export const Indent = styled.div` - padding: 0 4px; + padding: 0 12px; `; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -43,18 +53,27 @@ export const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` `; export const SummaryViewComponent: React.FC<{ - title?: string; + goToTable: () => void; + title: string; summaryColumns: Array>; summaryRows: SummaryRow[]; dataTestSubj?: string; -}> = ({ summaryColumns, summaryRows, dataTestSubj = 'summary-view', title }) => { +}> = ({ goToTable, summaryColumns, summaryRows, dataTestSubj = 'summary-view', title }) => { return ( - <> - {title && ( - -
{title}
-
- )} +
+ + + +
{title}
+
+
+ + + {VIEW_ALL_DOCUMENT_FIELDS} + + +
+ - +
); }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx index 74d46cf3431dc..b49aafea92245 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx @@ -8,27 +8,22 @@ import React, { useCallback, useState, useContext } from 'react'; import { HoverActions } from '../../hover_actions'; import { useActionCellDataProvider } from './use_action_cell_data_provider'; -import { EventFieldsData, FieldsData } from '../types'; +import { EnrichedFieldInfo } from '../types'; import { ColumnHeaderOptions } from '../../../../../common/types/timeline'; -import { BrowserField } from '../../../containers/source'; import { TimelineContext } from '../../../../../../timelines/public'; -interface Props { +interface Props extends EnrichedFieldInfo { contextId: string; - data: FieldsData | EventFieldsData; + applyWidthAndPadding?: boolean; disabled?: boolean; - eventId: string; - fieldFromBrowserField?: BrowserField; getLinkValue?: (field: string) => string | null; - linkValue?: string | null | undefined; onFilterAdded?: () => void; - timelineId?: string; toggleColumn?: (column: ColumnHeaderOptions) => void; - values: string[] | null | undefined; } export const ActionCell: React.FC = React.memo( ({ + applyWidthAndPadding = true, contextId, data, eventId, @@ -68,6 +63,7 @@ export const ActionCell: React.FC = React.memo( return ( ` - min-width: ${({ $hideTopN }) => `${$hideTopN ? '112px' : '138px'}`}; - padding: ${(props) => `0 ${props.theme.eui.paddingSizes.s}`}; display: flex; ${(props) => @@ -82,8 +80,14 @@ const StyledHoverActionsContainer = styled.div<{ : ''} `; +const StyledHoverActionsContainerWithPaddingsAndMinWidth = styled(StyledHoverActionsContainer)` + min-width: ${({ $hideTopN }) => `${$hideTopN ? '112px' : '138px'}`}; + padding: ${(props) => `0 ${props.theme.eui.paddingSizes.s}`}; +`; + interface Props { additionalContent?: React.ReactNode; + applyWidthAndPadding?: boolean; closeTopN?: () => void; closePopOver?: () => void; dataProvider?: DataProvider | DataProvider[]; @@ -128,6 +132,7 @@ export const HoverActions: React.FC = React.memo( dataType, draggableId, enableOverflowButton = false, + applyWidthAndPadding = true, field, goGetTimelineId, isObjectArray, @@ -227,6 +232,10 @@ export const HoverActions: React.FC = React.memo( values, }); + const Container = applyWidthAndPadding + ? StyledHoverActionsContainerWithPaddingsAndMinWidth + : StyledHoverActionsContainer; + return ( = React.memo( showTopN, })} > - = React.memo( {additionalContent != null && {additionalContent}} {enableOverflowButton && !isCaseView ? overflowActionItems : allActionItems} - + ); } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index 309c6c7f9761c..1897ad45fe7ff 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -265,6 +265,20 @@ export const STATUS = i18n.translate( } ); +export const CHANGE_ALERT_STATUS = i18n.translate( + 'xpack.securitySolution.eventsViewer.alerts.overview.changeAlertStatus', + { + defaultMessage: 'Change alert status', + } +); + +export const CLICK_TO_CHANGE_ALERT_STATUS = i18n.translate( + 'xpack.securitySolution.eventsViewer.alerts.overview.clickToChangeAlertStatus', + { + defaultMessage: 'Click to change alert status', + } +); + export const SIGNAL_STATUS = i18n.translate( 'xpack.securitySolution.eventsViewer.alerts.overviewTable.signalStatusTitle', { @@ -278,10 +292,3 @@ export const TRIGGERED = i18n.translate( defaultMessage: 'Triggered', } ); - -export const TIMESTAMP = i18n.translate( - 'xpack.securitySolution.eventsViewer.alerts.overviewTable.timestampTitle', - { - defaultMessage: 'Timestamp', - } -); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 06dc3ea3ed967..80d8e8f9b9e26 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -439,6 +439,7 @@ exports[`Details Panel Component DetailsPanel:EventDetails: rendering it should "indexName": "my-index", } } + handleOnEventClosed={[Function]} hostRisk={null} isAlert={false} isDraggable={false} @@ -760,6 +761,7 @@ Array [ isAlert={false} loading={true} ruleName="" + timestamp="" > void; interface Props { @@ -37,12 +40,14 @@ interface Props { timelineTabType: TimelineTabs | 'flyout'; timelineId: string; hostRisk: HostRisk | null; + handleOnEventClosed: HandleOnEventClosed; } interface ExpandableEventTitleProps { isAlert: boolean; loading: boolean; ruleName?: string; + timestamp?: string; handleOnEventClosed?: HandleOnEventClosed; } @@ -63,13 +68,22 @@ const StyledEuiFlexItem = styled(EuiFlexItem)` `; export const ExpandableEventTitle = React.memo( - ({ isAlert, loading, handleOnEventClosed, ruleName }) => ( + ({ isAlert, loading, handleOnEventClosed, ruleName, timestamp }) => ( {!loading && ( - -

{isAlert && !isEmpty(ruleName) ? ruleName : i18n.EVENT_DETAILS}

-
+ <> + +

{isAlert && !isEmpty(ruleName) ? ruleName : i18n.EVENT_DETAILS}

+
+ {timestamp && ( + <> + + + + )} + + )}
{handleOnEventClosed && ( @@ -95,6 +109,7 @@ export const ExpandableEvent = React.memo( detailsData, hostRisk, rawEventData, + handleOnEventClosed, }) => { if (!event.eventId) { return {i18n.EVENT_DETAILS_PLACEHOLDER}; @@ -112,11 +127,13 @@ export const ExpandableEvent = React.memo( data={detailsData ?? []} id={event.eventId} isAlert={isAlert} + indexName={event.indexName} isDraggable={isDraggable} rawEventData={rawEventData} timelineId={timelineId} timelineTabType={timelineTabType} hostRisk={hostRisk} + handleOnEventClosed={handleOnEventClosed} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 1d68356fc0bb7..4325e8ed64542 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -133,6 +133,11 @@ const EventDetailsPanelComponent: React.FC = ({ hostName, }); + const timestamp = useMemo( + () => getFieldValue({ category: 'base', field: '@timestamp' }, detailsData), + [detailsData] + ); + const backToAlertDetailsLink = useMemo(() => { return ( <> @@ -173,7 +178,12 @@ const EventDetailsPanelComponent: React.FC = ({ {isHostIsolationPanelOpen ? ( backToAlertDetailsLink ) : ( - + )} {isIsolateActionSuccessBannerVisible && ( @@ -203,6 +213,7 @@ const EventDetailsPanelComponent: React.FC = ({ timelineId={timelineId} timelineTabType="flyout" hostRisk={hostRisk} + handleOnEventClosed={handleOnEventClosed} /> )} @@ -237,6 +248,7 @@ const EventDetailsPanelComponent: React.FC = ({ timelineId={timelineId} timelineTabType={tabType} hostRisk={hostRisk} + handleOnEventClosed={handleOnEventClosed} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index 63e7e164854df..ffd8da99bb607 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -57,6 +57,7 @@ const FormattedFieldValueComponent: React.FC<{ isButton?: boolean; isDraggable?: boolean; onClick?: () => void; + onClickAriaLabel?: string; title?: string; truncate?: boolean; value: string | number | undefined | null; @@ -73,6 +74,7 @@ const FormattedFieldValueComponent: React.FC<{ isObjectArray = false, isDraggable = true, onClick, + onClickAriaLabel, title, truncate = true, value, @@ -190,6 +192,10 @@ const FormattedFieldValueComponent: React.FC<{ fieldName={fieldName} isDraggable={isDraggable} value={value} + onClick={onClick} + onClickAriaLabel={onClickAriaLabel} + iconType={isButton ? 'arrowDown' : undefined} + iconSide={isButton ? 'right' : undefined} /> ); } else if (fieldName === AGENT_STATUS_FIELD_NAME) { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx index d75bf436028f5..7f0d036812869 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx @@ -6,7 +6,7 @@ */ import React, { useMemo } from 'react'; -import { EuiBadge } from '@elastic/eui'; +import { EuiBadge, EuiBadgeProps } from '@elastic/eui'; import { getOr } from 'lodash/fp'; import styled from 'styled-components'; @@ -22,7 +22,7 @@ const StyledEuiBadge = styled(EuiBadge)` text-transform: capitalize; `; -interface Props { +interface BaseProps { contextId: string; eventId: string; fieldName: string; @@ -30,14 +30,33 @@ interface Props { value: string | number | undefined | null; } +type Props = BaseProps & + Pick; + const RuleStatusComponent: React.FC = ({ contextId, eventId, fieldName, isDraggable, value, + onClick, + onClickAriaLabel, + iconSide, + iconType, }) => { const color = useMemo(() => getOr('default', `${value}`, mapping), [value]); + const badge = ( + + {value} + + ); + return isDraggable ? ( = ({ value={`${value}`} tooltipContent={fieldName} > - {value} + {badge} ) : ( - {value} + badge ); }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 921794f6b6af0..b821888f0e397 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22890,7 +22890,6 @@ "xpack.securitySolution.eventsViewer.alerts.defaultHeaders.versionTitle": "バージョン", "xpack.securitySolution.eventsViewer.alerts.overviewTable.signalStatusTitle": "ステータス", "xpack.securitySolution.eventsViewer.alerts.overviewTable.targetImportHash": "ハッシュのインポート", - "xpack.securitySolution.eventsViewer.alerts.overviewTable.timestampTitle": "タイムスタンプ", "xpack.securitySolution.eventsViewer.errorFetchingEventsData": "イベントデータをクエリできませんでした", "xpack.securitySolution.eventsViewer.eventsLabel": "イベント", "xpack.securitySolution.eventsViewer.showingLabel": "表示中", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 66c2f192a6a3b..d3b1cc495da47 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23254,7 +23254,6 @@ "xpack.securitySolution.eventsViewer.alerts.defaultHeaders.versionTitle": "版本", "xpack.securitySolution.eventsViewer.alerts.overviewTable.signalStatusTitle": "状态", "xpack.securitySolution.eventsViewer.alerts.overviewTable.targetImportHash": "导入哈希", - "xpack.securitySolution.eventsViewer.alerts.overviewTable.timestampTitle": "时间戳", "xpack.securitySolution.eventsViewer.errorFetchingEventsData": "无法查询事件数据", "xpack.securitySolution.eventsViewer.eventsLabel": "事件", "xpack.securitySolution.eventsViewer.showingLabel": "正在显示", From ab2fdb17b1c38add71745ad3b62555c4d89d91dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 10 Dec 2021 13:25:58 +0000 Subject: [PATCH 017/145] @kbn/config: Clear empty-object properties after their props are unset (#120889) --- ...kibana-plugin-core-public.doclinksstart.md | 2 +- .../deprecation/apply_deprecations.test.ts | 30 ++++++++ .../src/deprecation/apply_deprecations.ts | 5 +- packages/kbn-config/src/deprecation/types.ts | 76 +++++++++++++++++++ .../unset_and_clean_empty_parent.test.ts | 41 ++++++++++ .../unset_and_clean_empty_parent.ts | 42 ++++++++++ .../data/server/config_deprecations.test.ts | 4 +- .../server/config_deprecations.test.ts | 2 +- 8 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 packages/kbn-config/src/deprecation/unset_and_clean_empty_parent.test.ts create mode 100644 packages/kbn-config/src/deprecation/unset_and_clean_empty_parent.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index a9828f04672e9..b60f9ad17e9c4 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | { readonly settings: string; readonly elasticStackGetStarted: string; readonly upgrade: { readonly upgradingElasticStack: string; }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; readonly customLinks: string; readonly droppedTransactionSpans: string; readonly upgrading: string; readonly metaData: string; }; readonly canvas: { readonly guide: string; }; readonly cloud: { readonly indexManagement: string; }; readonly dashboard: { readonly guide: string; readonly drilldowns: string; readonly drilldownsTriggerPicker: string; readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; readonly discover: Record<string, string>; readonly filebeat: { readonly base: string; readonly installation: string; readonly configuration: string; readonly elasticsearchOutput: string; readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; readonly suricataModule: string; readonly zeekModule: string; }; readonly auditbeat: { readonly base: string; readonly auditdModule: string; readonly systemModule: string; }; readonly metricbeat: { readonly base: string; readonly configure: string; readonly httpEndpoint: string; readonly install: string; readonly start: string; }; readonly appSearch: { readonly apiRef: string; readonly apiClients: string; readonly apiKeys: string; readonly authentication: string; readonly crawlRules: string; readonly curations: string; readonly duplicateDocuments: string; readonly entryPoints: string; readonly guide: string; readonly indexingDocuments: string; readonly indexingDocumentsSchema: string; readonly logSettings: string; readonly metaEngines: string; readonly recisionTuning: string; readonly relevanceTuning: string; readonly resultSettings: string; readonly searchUI: string; readonly security: string; readonly synonyms: string; readonly webCrawler: string; readonly webCrawlerEventLogs: string; }; readonly enterpriseSearch: { readonly configuration: string; readonly licenseManagement: string; readonly mailService: string; readonly usersAccess: string; }; readonly workplaceSearch: { readonly apiKeys: string; readonly box: string; readonly confluenceCloud: string; readonly confluenceServer: string; readonly customSources: string; readonly customSourcePermissions: string; readonly documentPermissions: string; readonly dropbox: string; readonly externalIdentities: string; readonly gitHub: string; readonly gettingStarted: string; readonly gmail: string; readonly googleDrive: string; readonly indexingSchedule: string; readonly jiraCloud: string; readonly jiraServer: string; readonly oneDrive: string; readonly permissions: string; readonly salesforce: string; readonly security: string; readonly serviceNow: string; readonly sharePoint: string; readonly slack: string; readonly synch: string; readonly zendesk: string; }; readonly heartbeat: { readonly base: string; }; readonly libbeat: { readonly getStarted: string; }; readonly logstash: { readonly base: string; }; readonly functionbeat: { readonly base: string; }; readonly winlogbeat: { readonly base: string; }; readonly aggs: { readonly composite: string; readonly composite\_missing\_bucket: string; readonly date\_histogram: string; readonly date\_range: string; readonly date\_format\_pattern: string; readonly filter: string; readonly filters: string; readonly geohash\_grid: string; readonly histogram: string; readonly ip\_range: string; readonly range: string; readonly significant\_terms: string; readonly terms: string; readonly terms\_doc\_count\_error: string; readonly avg: string; readonly avg\_bucket: string; readonly max\_bucket: string; readonly min\_bucket: string; readonly sum\_bucket: string; readonly cardinality: string; readonly count: string; readonly cumulative\_sum: string; readonly derivative: string; readonly geo\_bounds: string; readonly geo\_centroid: string; readonly max: string; readonly median: string; readonly min: string; readonly moving\_avg: string; readonly percentile\_ranks: string; readonly serial\_diff: string; readonly std\_dev: string; readonly sum: string; readonly top\_hits: string; }; readonly runtimeFields: { readonly overview: string; readonly mapping: string; }; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; readonly painless: string; readonly painlessApi: string; readonly painlessLangSpec: string; readonly painlessSyntax: string; readonly painlessWalkthrough: string; readonly luceneExpressions: string; }; readonly search: { readonly sessions: string; readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; readonly runtimeFields: string; }; readonly addData: string; readonly kibana: string; readonly upgradeAssistant: { readonly overview: string; readonly batchReindex: string; readonly remoteReindex: string; }; readonly rollupJobs: string; readonly elasticsearch: Record<string, string>; readonly siem: { readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; readonly ruleChangeLog: string; readonly detectionsReq: string; readonly networkMap: string; readonly troubleshootGaps: string; }; readonly securitySolution: { readonly trustedApps: string; }; readonly query: { readonly eql: string; readonly kueryQuerySyntax: string; readonly luceneQuerySyntax: string; readonly percolate: string; readonly queryDsl: string; }; readonly date: { readonly dateMath: string; readonly dateMathIndexNames: string; }; readonly management: Record<string, string>; readonly ml: Record<string, string>; readonly transforms: Record<string, string>; readonly visualize: Record<string, string>; readonly apis: Readonly<{ bulkIndexAlias: string; byteSizeUnits: string; createAutoFollowPattern: string; createFollower: string; createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; putComponentTemplate: string; painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putIndexTemplateV1: string; putWatch: string; simulatePipeline: string; timeUnits: string; updateTransform: string; }>; readonly observability: Readonly<{ guide: string; infrastructureThreshold: string; logsThreshold: string; metricsThreshold: string; monitorStatus: string; monitorUptime: string; tlsCertificate: string; uptimeDurationAnomaly: string; }>; readonly alerting: Record<string, string>; readonly maps: Readonly<{ guide: string; importGeospatialPrivileges: string; gdalTutorial: string; }>; readonly monitoring: Record<string, string>; readonly security: Readonly<{ apiKeyServiceSettings: string; clusterPrivileges: string; elasticsearchSettings: string; elasticsearchEnableSecurity: string; elasticsearchEnableApiKeys: string; indicesPrivileges: string; kibanaTLS: string; kibanaPrivileges: string; mappingRoles: string; mappingRolesFieldRules: string; runAsPrivilege: string; }>; readonly spaces: Readonly<{ kibanaLegacyUrlAliases: string; kibanaDisableLegacyUrlAliasesApi: string; }>; readonly watcher: Record<string, string>; readonly ccs: Record<string, string>; readonly plugins: Record<string, string>; readonly snapshotRestore: Record<string, string>; readonly ingest: Record<string, string>; readonly fleet: Readonly<{ beatsAgentComparison: string; guide: string; fleetServer: string; fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; datastreamsNamingScheme: string; installElasticAgent: string; installElasticAgentStandalone: string; upgradeElasticAgent: string; upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; onPremRegistry: string; }>; readonly ecs: { readonly guide: string; }; readonly clients: { readonly guide: string; readonly goOverview: string; readonly javaIndex: string; readonly jsIntro: string; readonly netGuide: string; readonly perlGuide: string; readonly phpGuide: string; readonly pythonGuide: string; readonly rubyOverview: string; readonly rustGuide: string; }; readonly endpoints: { readonly troubleshooting: string; }; } | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | { readonly settings: string; readonly elasticStackGetStarted: string; readonly upgrade: { readonly upgradingElasticStack: string; }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; readonly customLinks: string; readonly droppedTransactionSpans: string; readonly upgrading: string; readonly metaData: string; }; readonly canvas: { readonly guide: string; }; readonly cloud: { readonly indexManagement: string; }; readonly dashboard: { readonly guide: string; readonly drilldowns: string; readonly drilldownsTriggerPicker: string; readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; readonly discover: Record<string, string>; readonly filebeat: { readonly base: string; readonly installation: string; readonly configuration: string; readonly elasticsearchOutput: string; readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; readonly suricataModule: string; readonly zeekModule: string; }; readonly auditbeat: { readonly base: string; readonly auditdModule: string; readonly systemModule: string; }; readonly metricbeat: { readonly base: string; readonly configure: string; readonly httpEndpoint: string; readonly install: string; readonly start: string; }; readonly appSearch: { readonly apiRef: string; readonly apiClients: string; readonly apiKeys: string; readonly authentication: string; readonly crawlRules: string; readonly curations: string; readonly duplicateDocuments: string; readonly entryPoints: string; readonly guide: string; readonly indexingDocuments: string; readonly indexingDocumentsSchema: string; readonly logSettings: string; readonly metaEngines: string; readonly precisionTuning: string; readonly relevanceTuning: string; readonly resultSettings: string; readonly searchUI: string; readonly security: string; readonly synonyms: string; readonly webCrawler: string; readonly webCrawlerEventLogs: string; }; readonly enterpriseSearch: { readonly configuration: string; readonly licenseManagement: string; readonly mailService: string; readonly usersAccess: string; }; readonly workplaceSearch: { readonly apiKeys: string; readonly box: string; readonly confluenceCloud: string; readonly confluenceServer: string; readonly customSources: string; readonly customSourcePermissions: string; readonly documentPermissions: string; readonly dropbox: string; readonly externalIdentities: string; readonly gitHub: string; readonly gettingStarted: string; readonly gmail: string; readonly googleDrive: string; readonly indexingSchedule: string; readonly jiraCloud: string; readonly jiraServer: string; readonly oneDrive: string; readonly permissions: string; readonly salesforce: string; readonly security: string; readonly serviceNow: string; readonly sharePoint: string; readonly slack: string; readonly synch: string; readonly zendesk: string; }; readonly heartbeat: { readonly base: string; }; readonly libbeat: { readonly getStarted: string; }; readonly logstash: { readonly base: string; }; readonly functionbeat: { readonly base: string; }; readonly winlogbeat: { readonly base: string; }; readonly aggs: { readonly composite: string; readonly composite\_missing\_bucket: string; readonly date\_histogram: string; readonly date\_range: string; readonly date\_format\_pattern: string; readonly filter: string; readonly filters: string; readonly geohash\_grid: string; readonly histogram: string; readonly ip\_range: string; readonly range: string; readonly significant\_terms: string; readonly terms: string; readonly terms\_doc\_count\_error: string; readonly avg: string; readonly avg\_bucket: string; readonly max\_bucket: string; readonly min\_bucket: string; readonly sum\_bucket: string; readonly cardinality: string; readonly count: string; readonly cumulative\_sum: string; readonly derivative: string; readonly geo\_bounds: string; readonly geo\_centroid: string; readonly max: string; readonly median: string; readonly min: string; readonly moving\_avg: string; readonly percentile\_ranks: string; readonly serial\_diff: string; readonly std\_dev: string; readonly sum: string; readonly top\_hits: string; }; readonly runtimeFields: { readonly overview: string; readonly mapping: string; }; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; readonly painless: string; readonly painlessApi: string; readonly painlessLangSpec: string; readonly painlessSyntax: string; readonly painlessWalkthrough: string; readonly luceneExpressions: string; }; readonly search: { readonly sessions: string; readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; readonly runtimeFields: string; }; readonly addData: string; readonly kibana: string; readonly upgradeAssistant: { readonly overview: string; readonly batchReindex: string; readonly remoteReindex: string; }; readonly rollupJobs: string; readonly elasticsearch: Record<string, string>; readonly siem: { readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; readonly ruleChangeLog: string; readonly detectionsReq: string; readonly networkMap: string; readonly troubleshootGaps: string; }; readonly securitySolution: { readonly trustedApps: string; }; readonly query: { readonly eql: string; readonly kueryQuerySyntax: string; readonly luceneQuerySyntax: string; readonly percolate: string; readonly queryDsl: string; }; readonly date: { readonly dateMath: string; readonly dateMathIndexNames: string; }; readonly management: Record<string, string>; readonly ml: Record<string, string>; readonly transforms: Record<string, string>; readonly visualize: Record<string, string>; readonly apis: Readonly<{ bulkIndexAlias: string; byteSizeUnits: string; createAutoFollowPattern: string; createFollower: string; createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; putComponentTemplate: string; painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putIndexTemplateV1: string; putWatch: string; simulatePipeline: string; timeUnits: string; updateTransform: string; }>; readonly observability: Readonly<{ guide: string; infrastructureThreshold: string; logsThreshold: string; metricsThreshold: string; monitorStatus: string; monitorUptime: string; tlsCertificate: string; uptimeDurationAnomaly: string; }>; readonly alerting: Record<string, string>; readonly maps: Readonly<{ guide: string; importGeospatialPrivileges: string; gdalTutorial: string; }>; readonly monitoring: Record<string, string>; readonly security: Readonly<{ apiKeyServiceSettings: string; clusterPrivileges: string; elasticsearchSettings: string; elasticsearchEnableSecurity: string; elasticsearchEnableApiKeys: string; indicesPrivileges: string; kibanaTLS: string; kibanaPrivileges: string; mappingRoles: string; mappingRolesFieldRules: string; runAsPrivilege: string; }>; readonly spaces: Readonly<{ kibanaLegacyUrlAliases: string; kibanaDisableLegacyUrlAliasesApi: string; }>; readonly watcher: Record<string, string>; readonly ccs: Record<string, string>; readonly plugins: { azureRepo: string; gcsRepo: string; hdfsRepo: string; s3Repo: string; snapshotRestoreRepos: string; mapperSize: string; }; readonly snapshotRestore: Record<string, string>; readonly ingest: Record<string, string>; readonly fleet: Readonly<{ beatsAgentComparison: string; guide: string; fleetServer: string; fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; datastreamsNamingScheme: string; installElasticAgent: string; installElasticAgentStandalone: string; upgradeElasticAgent: string; upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; onPremRegistry: string; }>; readonly ecs: { readonly guide: string; }; readonly clients: { readonly guide: string; readonly goOverview: string; readonly javaIndex: string; readonly jsIntro: string; readonly netGuide: string; readonly perlGuide: string; readonly phpGuide: string; readonly pythonGuide: string; readonly rubyOverview: string; readonly rustGuide: string; }; readonly endpoints: { readonly troubleshooting: string; }; } | | diff --git a/packages/kbn-config/src/deprecation/apply_deprecations.test.ts b/packages/kbn-config/src/deprecation/apply_deprecations.test.ts index 70945b2d96b32..3f84eed867655 100644 --- a/packages/kbn-config/src/deprecation/apply_deprecations.test.ts +++ b/packages/kbn-config/src/deprecation/apply_deprecations.test.ts @@ -116,6 +116,36 @@ describe('applyDeprecations', () => { expect(migrated).toEqual({ foo: 'bar', newname: 'renamed' }); }); + it('nested properties take into account if their parents are empty objects, and remove them if so', () => { + const initialConfig = { + foo: 'bar', + deprecated: { nested: 'deprecated' }, + nested: { + from: { + rename: 'renamed', + }, + to: { + keep: 'keep', + }, + }, + }; + + const { config: migrated } = applyDeprecations(initialConfig, [ + wrapHandler(deprecations.unused('deprecated.nested')), + wrapHandler(deprecations.rename('nested.from.rename', 'nested.to.renamed')), + ]); + + expect(migrated).toStrictEqual({ + foo: 'bar', + nested: { + to: { + keep: 'keep', + renamed: 'renamed', + }, + }, + }); + }); + it('does not alter the initial config', () => { const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; diff --git a/packages/kbn-config/src/deprecation/apply_deprecations.ts b/packages/kbn-config/src/deprecation/apply_deprecations.ts index 11b35840969d0..9b0c409204414 100644 --- a/packages/kbn-config/src/deprecation/apply_deprecations.ts +++ b/packages/kbn-config/src/deprecation/apply_deprecations.ts @@ -6,13 +6,14 @@ * Side Public License, v 1. */ -import { cloneDeep, unset } from 'lodash'; +import { cloneDeep } from 'lodash'; import { set } from '@elastic/safer-lodash-set'; import type { AddConfigDeprecation, ChangedDeprecatedPaths, ConfigDeprecationWithContext, } from './types'; +import { unsetAndCleanEmptyParent } from './unset_and_clean_empty_parent'; const noopAddDeprecationFactory: () => AddConfigDeprecation = () => () => undefined; @@ -45,7 +46,7 @@ export const applyDeprecations = ( if (commands.unset) { changedPaths.unset.push(...commands.unset.map((c) => c.path)); commands.unset.forEach(function ({ path: commandPath }) { - unset(result, commandPath); + unsetAndCleanEmptyParent(result, commandPath); }); } } diff --git a/packages/kbn-config/src/deprecation/types.ts b/packages/kbn-config/src/deprecation/types.ts index 7b1eb4a0ea6c1..6abe4cd94a6fb 100644 --- a/packages/kbn-config/src/deprecation/types.ts +++ b/packages/kbn-config/src/deprecation/types.ts @@ -186,6 +186,25 @@ export interface ConfigDeprecationFactory { * rename('oldKey', 'newKey'), * ] * ``` + * + * @remarks + * If the oldKey is a nested property and it's the last property in an object, it may remove any empty-object parent keys. + * ``` + * // Original object + * { + * a: { + * b: { c: 1 }, + * d: { e: 1 } + * } + * } + * + * // If rename('a.b.c', 'a.d.c'), the resulting object removes the entire "a.b" tree because "c" was the last property in that branch + * { + * a: { + * d: { c: 1, e: 1 } + * } + * } + * ``` */ rename( oldKey: string, @@ -207,6 +226,25 @@ export interface ConfigDeprecationFactory { * renameFromRoot('oldplugin.key', 'newplugin.key'), * ] * ``` + * + * @remarks + * If the oldKey is a nested property and it's the last property in an object, it may remove any empty-object parent keys. + * ``` + * // Original object + * { + * a: { + * b: { c: 1 }, + * d: { e: 1 } + * } + * } + * + * // If renameFromRoot('a.b.c', 'a.d.c'), the resulting object removes the entire "a.b" tree because "c" was the last property in that branch + * { + * a: { + * d: { c: 1, e: 1 } + * } + * } + * ``` */ renameFromRoot( oldKey: string, @@ -225,6 +263,25 @@ export interface ConfigDeprecationFactory { * unused('deprecatedKey'), * ] * ``` + * + * @remarks + * If the path is a nested property and it's the last property in an object, it may remove any empty-object parent keys. + * ``` + * // Original object + * { + * a: { + * b: { c: 1 }, + * d: { e: 1 } + * } + * } + * + * // If unused('a.b.c'), the resulting object removes the entire "a.b" tree because "c" was the last property in that branch + * { + * a: { + * d: { e: 1 } + * } + * } + * ``` */ unused(unusedKey: string, details?: Partial): ConfigDeprecation; @@ -242,6 +299,25 @@ export interface ConfigDeprecationFactory { * unusedFromRoot('somepath.deprecatedProperty'), * ] * ``` + * + * @remarks + * If the path is a nested property and it's the last property in an object, it may remove any empty-object parent keys. + * ``` + * // Original object + * { + * a: { + * b: { c: 1 }, + * d: { e: 1 } + * } + * } + * + * // If unused('a.b.c'), the resulting object removes the entire "a.b" tree because "c" was the last property in that branch + * { + * a: { + * d: { e: 1 } + * } + * } + * ``` */ unusedFromRoot(unusedKey: string, details?: Partial): ConfigDeprecation; } diff --git a/packages/kbn-config/src/deprecation/unset_and_clean_empty_parent.test.ts b/packages/kbn-config/src/deprecation/unset_and_clean_empty_parent.test.ts new file mode 100644 index 0000000000000..115730c106137 --- /dev/null +++ b/packages/kbn-config/src/deprecation/unset_and_clean_empty_parent.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { unsetAndCleanEmptyParent } from './unset_and_clean_empty_parent'; + +describe('unsetAndcleanEmptyParent', () => { + test('unsets the property of the root object, and returns an empty root object', () => { + const config = { toRemove: 'toRemove' }; + unsetAndCleanEmptyParent(config, 'toRemove'); + expect(config).toStrictEqual({}); + }); + + test('unsets a nested property of the root object, and removes the empty parent property', () => { + const config = { nestedToRemove: { toRemove: 'toRemove' } }; + unsetAndCleanEmptyParent(config, 'nestedToRemove.toRemove'); + expect(config).toStrictEqual({}); + }); + + describe('Navigating to parent known issue: Array paths', () => { + // We navigate to the parent property by splitting the "." and dropping the last item in the path. + // This means that paths that are declared as prop1[idx] cannot apply the parent's cleanup logic. + // The use cases for this are quite limited, so we'll accept it as a documented limitation. + + test('does not remove a parent array when the index is specified with square brackets', () => { + const config = { nestedToRemove: [{ toRemove: 'toRemove' }] }; + unsetAndCleanEmptyParent(config, 'nestedToRemove[0].toRemove'); + expect(config).toStrictEqual({ nestedToRemove: [{}] }); + }); + + test('removes a parent array when the index is specified with dots', () => { + const config = { nestedToRemove: [{ toRemove: 'toRemove' }] }; + unsetAndCleanEmptyParent(config, 'nestedToRemove.0.toRemove'); + expect(config).toStrictEqual({}); + }); + }); +}); diff --git a/packages/kbn-config/src/deprecation/unset_and_clean_empty_parent.ts b/packages/kbn-config/src/deprecation/unset_and_clean_empty_parent.ts new file mode 100644 index 0000000000000..c5f5e5951adc4 --- /dev/null +++ b/packages/kbn-config/src/deprecation/unset_and_clean_empty_parent.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { get, unset } from 'lodash'; + +/** + * Unsets the path and checks if the parent property is an empty object. + * If so, it removes the property from the config object (mutation is applied). + * + * @internal + */ +export const unsetAndCleanEmptyParent = ( + config: Record, + path: string | string[] +): void => { + // 1. Unset the provided path + const didUnset = unset(config, path); + + // Check if the unset actually removed anything. + // This way we avoid some CPU cycles when the previous action didn't apply any changes. + if (didUnset) { + // 2. Check if the parent property in the resulting object is an empty object + const pathArray = Array.isArray(path) ? path : path.split('.'); + const parentPath = pathArray.slice(0, -1); + if (parentPath.length === 0) { + return; + } + const parentObj = get(config, parentPath); + if ( + typeof parentObj === 'object' && + parentObj !== null && + Object.keys(parentObj).length === 0 + ) { + unsetAndCleanEmptyParent(config, parentPath); + } + } +}; diff --git a/src/plugins/data/server/config_deprecations.test.ts b/src/plugins/data/server/config_deprecations.test.ts index 6c09b060aa763..3df1ea9119292 100644 --- a/src/plugins/data/server/config_deprecations.test.ts +++ b/src/plugins/data/server/config_deprecations.test.ts @@ -50,7 +50,7 @@ describe('Config Deprecations', () => { }, }; const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); - expect(migrated.kibana.autocompleteTerminateAfter).not.toBeDefined(); + expect(migrated.kibana?.autocompleteTerminateAfter).not.toBeDefined(); expect(migrated.data.autocomplete.valueSuggestions.terminateAfter).toEqual(123); expect(messages).toMatchInlineSnapshot(` Array [ @@ -66,7 +66,7 @@ describe('Config Deprecations', () => { }, }; const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); - expect(migrated.kibana.autocompleteTimeout).not.toBeDefined(); + expect(migrated.kibana?.autocompleteTimeout).not.toBeDefined(); expect(migrated.data.autocomplete.valueSuggestions.timeout).toEqual(123); expect(messages).toMatchInlineSnapshot(` Array [ diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts index 7a85e614e4b62..150e878f4297f 100644 --- a/x-pack/plugins/security/server/config_deprecations.test.ts +++ b/x-pack/plugins/security/server/config_deprecations.test.ts @@ -182,7 +182,7 @@ describe('Config Deprecations', () => { }, }; const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); - expect(migrated.security.showInsecureClusterWarning).not.toBeDefined(); + expect(migrated.security?.showInsecureClusterWarning).not.toBeDefined(); expect(migrated.xpack.security.showInsecureClusterWarning).toEqual(false); expect(messages).toMatchInlineSnapshot(` Array [ From 3129b98313e55e0e1b9508a37a0d67b20067d7a6 Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Fri, 10 Dec 2021 16:51:12 -0500 Subject: [PATCH 018/145] Added Close Index Component Integration Test For Index Management (#114020) * Added close index test. * Fixed linting issues. * Fixed linting issues. * Abstracted out the index action option selection method and cleaned up test. * Merged Yulia's changes into this PR and updated the test to consume the new data test subjects. * Adjusted assertion to check for second to last request since there is a refresh done after the close index call. * Fixed linting issue. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../client_integration/helpers/test_subjects.ts | 1 - .../client_integration/home/indices_tab.helpers.ts | 1 - .../client_integration/home/indices_tab.test.ts | 13 ++++++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts index ac4b4c46ad4d1..5da1fc61742e6 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts @@ -25,7 +25,6 @@ export type TestSubjects = | 'ilmPolicyLink' | 'includeStatsSwitch' | 'includeManagedSwitch' - | 'indexActionsContextMenuButton' | 'indexContextMenu' | 'indexManagementHeaderContent' | 'indexTable' diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts index 0e4564163c553..c1b8dfcc0034f 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts @@ -45,7 +45,6 @@ export const setup = async (overridingDependencies: any = {}): Promise { const { find, component } = testBed; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts index ec80bf5d712c0..689c48b24a9c3 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -196,11 +196,22 @@ describe('', () => { httpRequestsMockHelpers.setReloadIndicesResponse({ indexNames: [indexName] }); testBed = await setup(); - const { find, component } = testBed; + const { component, find } = testBed; + component.update(); find('indexTableIndexNameLink').at(0).simulate('click'); }); + test('should be able to close an open index', async () => { + const { actions } = testBed; + + await actions.clickManageContextMenuButton(); + await actions.clickContextMenuOption('closeIndexMenuButton'); + + // A refresh call was added after closing an index so we need to check the second to last request. + const latestRequest = server.requests[server.requests.length - 2]; + expect(latestRequest.url).toBe(`${API_BASE_PATH}/indices/close`); + }); test('should be able to flush index', async () => { const { actions } = testBed; From c4e8cb3594b0b5dd81e0cda735831508c26afa81 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Mon, 13 Dec 2021 09:24:38 +0100 Subject: [PATCH 019/145] [Discover] Rename IndexPatternFieldEditor > DataViewFieldEditor (#120344) * [Discover] Rename IndexPatternFieldEditor > DataViewFieldEditor * Update remaining test * Rename editIndexPatternField > editDataViewField * Fix wrong variable name Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../discover/public/__mocks__/services.ts | 2 +- ...ver_index_pattern_management.test.tsx.snap | 4 +-- ...discover_index_pattern_management.test.tsx | 2 +- .../discover_index_pattern_management.tsx | 9 +++---- .../components/sidebar/discover_sidebar.tsx | 27 +++++++++---------- .../sidebar/discover_sidebar_responsive.tsx | 8 +++--- src/plugins/discover/public/build_services.ts | 4 +-- 7 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index ec7657827d95b..337d44227139e 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -78,7 +78,7 @@ export const discoverServiceMock = { http: { basePath: '/', }, - indexPatternFieldEditor: { + dataViewFieldEditor: { openEditor: jest.fn(), userPermissions: { editIndexPattern: jest.fn(), diff --git a/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap b/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap index 6cb6a15aa0f66..17d414215af55 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap +++ b/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap @@ -653,13 +653,13 @@ exports[`Discover IndexPattern Management renders correctly 1`] = ` "navigateToApp": [MockFunction], }, }, - "history": [Function], - "indexPatternFieldEditor": Object { + "dataViewFieldEditor": Object { "openEditor": [MockFunction], "userPermissions": Object { "editIndexPattern": [Function], }, }, + "history": [Function], "uiSettings": Object { "get": [Function], }, diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.test.tsx index c5b1f4d2612d6..4132e4fb1b9b8 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.test.tsx @@ -39,7 +39,7 @@ const mockServices = { } }, }, - indexPatternFieldEditor: { + dataViewFieldEditor: { openEditor: jest.fn(), userPermissions: { editIndexPattern: () => { diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx index 9353073e7fad6..7fbb518ca3034 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx @@ -33,14 +33,13 @@ export interface DiscoverIndexPatternManagementProps { } export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManagementProps) { - const { indexPatternFieldEditor, core } = props.services; + const { dataViewFieldEditor, core } = props.services; const { useNewFieldsApi, selectedIndexPattern, editField } = props; - const indexPatternFieldEditPermission = - indexPatternFieldEditor?.userPermissions.editIndexPattern(); - const canEditIndexPatternField = !!indexPatternFieldEditPermission && useNewFieldsApi; + const dataViewEditPermission = dataViewFieldEditor?.userPermissions.editIndexPattern(); + const canEditDataViewField = !!dataViewEditPermission && useNewFieldsApi; const [isAddIndexPatternFieldPopoverOpen, setIsAddIndexPatternFieldPopoverOpen] = useState(false); - if (!useNewFieldsApi || !selectedIndexPattern || !canEditIndexPatternField) { + if (!useNewFieldsApi || !selectedIndexPattern || !canEditDataViewField) { return null; } diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx index 78aee49d1b288..ea7b6fd31923e 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx @@ -109,10 +109,9 @@ export function DiscoverSidebarComponent({ }: DiscoverSidebarProps) { const [fields, setFields] = useState(null); - const { indexPatternFieldEditor } = services; - const indexPatternFieldEditPermission = - indexPatternFieldEditor?.userPermissions.editIndexPattern(); - const canEditIndexPatternField = !!indexPatternFieldEditPermission && useNewFieldsApi; + const { dataViewFieldEditor } = services; + const dataViewFieldEditPermission = dataViewFieldEditor?.userPermissions.editIndexPattern(); + const canEditDataViewField = !!dataViewFieldEditPermission && useNewFieldsApi; const [scrollContainer, setScrollContainer] = useState(null); const [fieldsToRender, setFieldsToRender] = useState(FIELDS_PER_PAGE); const [fieldsPerPage, setFieldsPerPage] = useState(FIELDS_PER_PAGE); @@ -243,9 +242,9 @@ export function DiscoverSidebarComponent({ const deleteField = useMemo( () => - canEditIndexPatternField && selectedIndexPattern + canEditDataViewField && selectedIndexPattern ? async (fieldName: string) => { - const ref = indexPatternFieldEditor.openDeleteModal({ + const ref = dataViewFieldEditor.openDeleteModal({ ctx: { dataView: selectedIndexPattern, }, @@ -264,11 +263,11 @@ export function DiscoverSidebarComponent({ : undefined, [ selectedIndexPattern, - canEditIndexPatternField, + canEditDataViewField, setFieldEditorRef, closeFlyout, onEditRuntimeField, - indexPatternFieldEditor, + dataViewFieldEditor, ] ); @@ -413,8 +412,8 @@ export function DiscoverSidebarComponent({ selected={true} trackUiMetric={trackUiMetric} multiFields={multiFields?.get(field.name)} - onEditField={canEditIndexPatternField ? editField : undefined} - onDeleteField={canEditIndexPatternField ? deleteField : undefined} + onEditField={canEditDataViewField ? editField : undefined} + onDeleteField={canEditDataViewField ? deleteField : undefined} showFieldStats={showFieldStats} /> @@ -473,8 +472,8 @@ export function DiscoverSidebarComponent({ getDetails={getDetailsByField} trackUiMetric={trackUiMetric} multiFields={multiFields?.get(field.name)} - onEditField={canEditIndexPatternField ? editField : undefined} - onDeleteField={canEditIndexPatternField ? deleteField : undefined} + onEditField={canEditDataViewField ? editField : undefined} + onDeleteField={canEditDataViewField ? deleteField : undefined} showFieldStats={showFieldStats} /> @@ -502,8 +501,8 @@ export function DiscoverSidebarComponent({ getDetails={getDetailsByField} trackUiMetric={trackUiMetric} multiFields={multiFields?.get(field.name)} - onEditField={canEditIndexPatternField ? editField : undefined} - onDeleteField={canEditIndexPatternField ? deleteField : undefined} + onEditField={canEditDataViewField ? editField : undefined} + onDeleteField={canEditDataViewField ? deleteField : undefined} showFieldStats={showFieldStats} /> diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index a4e84bd831619..6316369ff4c6f 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -180,17 +180,17 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) setIsFlyoutVisible(false); }, []); - const { indexPatternFieldEditor } = props.services; + const { dataViewFieldEditor } = props.services; const editField = useCallback( (fieldName?: string) => { const indexPatternFieldEditPermission = - indexPatternFieldEditor?.userPermissions.editIndexPattern(); + dataViewFieldEditor?.userPermissions.editIndexPattern(); const canEditIndexPatternField = !!indexPatternFieldEditPermission && useNewFieldsApi; if (!canEditIndexPatternField || !selectedIndexPattern) { return; } - const ref = indexPatternFieldEditor.openEditor({ + const ref = dataViewFieldEditor.openEditor({ ctx: { dataView: selectedIndexPattern, }, @@ -208,7 +208,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) }, [ closeFlyout, - indexPatternFieldEditor, + dataViewFieldEditor, selectedIndexPattern, setFieldEditorRef, onEditRuntimeField, diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 9cc2eb78aafbe..9f21294efdfc1 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -66,7 +66,7 @@ export interface DiscoverServices { toastNotifications: ToastsStart; uiSettings: IUiSettingsClient; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; - indexPatternFieldEditor: IndexPatternFieldEditorStart; + dataViewFieldEditor: IndexPatternFieldEditorStart; http: HttpStart; storage: Storage; spaces?: SpacesApi; @@ -105,7 +105,7 @@ export function buildServices( uiSettings: core.uiSettings, storage, trackUiMetric: usageCollection?.reportUiCounter.bind(usageCollection, 'discover'), - indexPatternFieldEditor: plugins.dataViewFieldEditor, + dataViewFieldEditor: plugins.dataViewFieldEditor, http: core.http, spaces: plugins.spaces, }; From 1e159f2aee82028e1406488c5c68a017b6fc569d Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Mon, 13 Dec 2021 10:44:47 +0000 Subject: [PATCH 020/145] [ML] Transforms: Use KibanaThemeProvider in transform plugin (#120933) * [ML] Use KibanaThemeProvider in transform plugin * [ML] Add missing mock Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/app/__mocks__/app_dependencies.tsx | 3 +- x-pack/plugins/transform/public/app/app.tsx | 21 ++++++++----- .../transform/public/app/app_dependencies.tsx | 1 + .../toast_notification_text.test.tsx | 3 ++ .../components/toast_notification_text.tsx | 5 +++- .../public/app/hooks/use_delete_transform.tsx | 30 +++++++++++++++---- .../public/app/hooks/use_start_transform.tsx | 9 ++++-- .../public/app/hooks/use_stop_transform.tsx | 9 ++++-- .../public/app/mount_management_section.ts | 3 +- .../step_create/step_create_form.tsx | 24 +++++++++++---- .../step_details/step_details_form.tsx | 24 +++++++++++---- 11 files changed, 102 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx b/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx index ab38d05ec9f8f..6aab7b558cf4d 100644 --- a/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx +++ b/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx @@ -9,7 +9,7 @@ import { useContext } from 'react'; import type { ScopedHistory } from 'kibana/public'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { coreMock, themeServiceMock } from '../../../../../../src/core/public/mocks'; import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; import { savedObjectsPluginMock } from '../../../../../../src/plugins/saved_objects/public/mocks'; import { SharePluginStart } from '../../../../../../src/plugins/share/public'; @@ -39,6 +39,7 @@ const appDependencies: AppDependencies = { savedObjects: coreStart.savedObjects, storage: { get: jest.fn() } as unknown as Storage, overlays: coreStart.overlays, + theme: themeServiceMock.createStartContract(), http: coreSetup.http, history: {} as ScopedHistory, savedObjectsPlugin: savedObjectsPluginMock.createStartContract(), diff --git a/x-pack/plugins/transform/public/app/app.tsx b/x-pack/plugins/transform/public/app/app.tsx index fd14ab8440202..ec88882d16425 100644 --- a/x-pack/plugins/transform/public/app/app.tsx +++ b/x-pack/plugins/transform/public/app/app.tsx @@ -14,7 +14,10 @@ import { EuiErrorBoundary } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { + KibanaContextProvider, + KibanaThemeProvider, +} from '../../../../../src/plugins/kibana_react/public'; import { API_BASE_PATH } from '../../common/constants'; @@ -65,13 +68,15 @@ export const renderApp = (element: HTMLElement, appDependencies: AppDependencies render( - - - - - - - + + + + + + + + + , element ); diff --git a/x-pack/plugins/transform/public/app/app_dependencies.tsx b/x-pack/plugins/transform/public/app/app_dependencies.tsx index da1178e395720..89807fb2e1247 100644 --- a/x-pack/plugins/transform/public/app/app_dependencies.tsx +++ b/x-pack/plugins/transform/public/app/app_dependencies.tsx @@ -30,6 +30,7 @@ export interface AppDependencies { savedObjects: CoreStart['savedObjects']; storage: Storage; overlays: CoreStart['overlays']; + theme: CoreStart['theme']; history: ScopedHistory; savedObjectsPlugin: SavedObjectsStart; share: SharePluginStart; diff --git a/x-pack/plugins/transform/public/app/components/toast_notification_text.test.tsx b/x-pack/plugins/transform/public/app/components/toast_notification_text.test.tsx index b085492f07e75..2539f1cf9bc06 100644 --- a/x-pack/plugins/transform/public/app/components/toast_notification_text.test.tsx +++ b/x-pack/plugins/transform/public/app/components/toast_notification_text.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { CoreStart } from 'src/core/public'; +import { themeServiceMock } from 'src/core/public/mocks'; import { ToastNotificationText } from './toast_notification_text'; @@ -20,6 +21,7 @@ describe('ToastNotificationText', () => { const props = { overlays: {} as CoreStart['overlays'], text: 'a short text message', + theme: themeServiceMock.createStartContract(), }; const { container } = render(); expect(container.textContent).toBe('a short text message'); @@ -29,6 +31,7 @@ describe('ToastNotificationText', () => { const props = { overlays: {} as CoreStart['overlays'], text: 'a text message that is longer than 140 characters. a text message that is longer than 140 characters. a text message that is longer than 140 characters. ', + theme: themeServiceMock.createStartContract(), }; const { container } = render(); expect(container.textContent).toBe( diff --git a/x-pack/plugins/transform/public/app/components/toast_notification_text.tsx b/x-pack/plugins/transform/public/app/components/toast_notification_text.tsx index 2555105fe2530..aebe74a539a1c 100644 --- a/x-pack/plugins/transform/public/app/components/toast_notification_text.tsx +++ b/x-pack/plugins/transform/public/app/components/toast_notification_text.tsx @@ -29,6 +29,7 @@ const MAX_SIMPLE_MESSAGE_LENGTH = 140; // That's why we need to pass in `overlays` as a prop cannot get it via context. interface ToastNotificationTextProps { overlays: CoreStart['overlays']; + theme: CoreStart['theme']; text: any; previewTextLength?: number; } @@ -36,6 +37,7 @@ interface ToastNotificationTextProps { export const ToastNotificationText: FC = ({ overlays, text, + theme, previewTextLength, }) => { if (typeof text === 'string' && text.length <= MAX_SIMPLE_MESSAGE_LENGTH) { @@ -80,7 +82,8 @@ export const ToastNotificationText: FC = ({ })} - + , + { theme$: theme.theme$ } ) ); }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx index 9ea5856434c52..ff93f027fc3a4 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx @@ -120,7 +120,7 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => { type SuccessCountField = keyof Omit; export const useDeleteTransforms = () => { - const { overlays } = useAppDependencies(); + const { overlays, theme } = useAppDependencies(); const toastNotifications = useToastNotifications(); const api = useApi(); @@ -136,8 +136,10 @@ export const useDeleteTransforms = () => { + />, + { theme$: theme.theme$ } ), }); return; @@ -203,7 +205,13 @@ export const useDeleteTransforms = () => { values: { transformId }, }), text: toMountPoint( - + , + { theme$: theme.theme$ } ), }); } @@ -219,7 +227,13 @@ export const useDeleteTransforms = () => { } ), text: toMountPoint( - + , + { theme$: theme.theme$ } ), }); } @@ -235,7 +249,13 @@ export const useDeleteTransforms = () => { } ), text: toMountPoint( - + , + { theme$: theme.theme$ } ), }); } diff --git a/x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx index d808d4ab509fd..c8abd13db394d 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx @@ -23,7 +23,7 @@ import { ToastNotificationText } from '../components'; import { useApi } from './use_api'; export const useStartTransforms = () => { - const deps = useAppDependencies(); + const { overlays, theme } = useAppDependencies(); const toastNotifications = useToastNotifications(); const api = useApi(); @@ -39,7 +39,12 @@ export const useStartTransforms = () => { } ), text: toMountPoint( - + , + { theme$: theme.theme$ } ), }); return; diff --git a/x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx index 61e0c6dfbeebc..894ac757b396a 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx @@ -23,7 +23,7 @@ import { ToastNotificationText } from '../components'; import { useApi } from './use_api'; export const useStopTransforms = () => { - const deps = useAppDependencies(); + const { overlays, theme } = useAppDependencies(); const toastNotifications = useToastNotifications(); const api = useApi(); @@ -39,7 +39,12 @@ export const useStopTransforms = () => { } ), text: toMountPoint( - + , + { theme$: theme.theme$ } ), }); return; diff --git a/x-pack/plugins/transform/public/app/mount_management_section.ts b/x-pack/plugins/transform/public/app/mount_management_section.ts index 6e63094064584..2f47b6d70bc79 100644 --- a/x-pack/plugins/transform/public/app/mount_management_section.ts +++ b/x-pack/plugins/transform/public/app/mount_management_section.ts @@ -28,7 +28,7 @@ export async function mountManagementSection( const { http, notifications, getStartServices } = coreSetup; const startServices = await getStartServices(); const [core, plugins] = startServices; - const { application, chrome, docLinks, i18n, overlays, savedObjects, uiSettings } = core; + const { application, chrome, docLinks, i18n, overlays, theme, savedObjects, uiSettings } = core; const { data, share, spaces, triggersActionsUi } = plugins; const { docTitle } = chrome; @@ -47,6 +47,7 @@ export async function mountManagementSection( i18n, notifications, overlays, + theme, savedObjects, storage: localStorage, uiSettings, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 874dd149da3e0..728fcef23f8a0 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -136,6 +136,7 @@ export const StepCreateForm: FC = React.memo( // eslint-disable-next-line react-hooks/exhaustive-deps }, [created, started, indexPatternId]); + const { overlays, theme } = useAppDependencies(); const api = useApi(); async function createTransform() { @@ -160,9 +161,11 @@ export const StepCreateForm: FC = React.memo( }), text: toMountPoint( + />, + { theme$: theme.theme$ } ), }); setCreated(false); @@ -214,7 +217,12 @@ export const StepCreateForm: FC = React.memo( values: { transformId }, }), text: toMountPoint( - + , + { theme$: theme.theme$ } ), }); setStarted(false); @@ -275,7 +283,8 @@ export const StepCreateForm: FC = React.memo( values: { dataViewName }, }), text: toMountPoint( - + , + { theme$: theme.theme$ } ), }); setLoading(false); @@ -321,7 +330,12 @@ export const StepCreateForm: FC = React.memo( defaultMessage: 'An error occurred getting the progress percentage:', }), text: toMountPoint( - + , + { theme$: theme.theme$ } ), }); clearInterval(interval); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index 3e9f80de9152e..828db2d37d913 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -118,6 +118,7 @@ export const StepDetailsForm: FC = React.memo( [setIndexPatternTimeField, indexPatternAvailableTimeFields] ); + const { overlays, theme } = useAppDependencies(); const api = useApi(); // fetch existing transform IDs and indices once for form validation @@ -150,9 +151,11 @@ export const StepDetailsForm: FC = React.memo( }), text: toMountPoint( + />, + { theme$: theme.theme$ } ), }); } @@ -165,7 +168,12 @@ export const StepDetailsForm: FC = React.memo( defaultMessage: 'An error occurred getting the existing transform IDs:', }), text: toMountPoint( - + , + { theme$: theme.theme$ } ), }); } else { @@ -182,7 +190,12 @@ export const StepDetailsForm: FC = React.memo( defaultMessage: 'An error occurred getting the existing index names:', }), text: toMountPoint( - + , + { theme$: theme.theme$ } ), }); } @@ -195,7 +208,8 @@ export const StepDetailsForm: FC = React.memo( defaultMessage: 'An error occurred getting the existing data view titles:', }), text: toMountPoint( - + , + { theme$: theme.theme$ } ), }); } From 32d0e877249121364f7adef97bd2cee530feb152 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Mon, 13 Dec 2021 10:45:02 +0000 Subject: [PATCH 021/145] [ML] Use KibanaThemeProvider in ML plugin (#120892) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/ml/public/application/app.tsx | 20 +-- .../application/license/expired_warning.tsx | 13 +- .../jobs_list_page/jobs_list_page.tsx | 126 +++++++++--------- .../models_management/force_stop_dialog.tsx | 31 +++-- .../models_management/models_list.tsx | 3 +- .../application/util/dependency_cache.ts | 11 ++ .../anomaly_charts_embeddable.tsx | 34 +++-- .../anomaly_charts_setup_flyout.tsx | 38 +++--- .../anomaly_swimlane_embeddable.tsx | 34 +++-- .../anomaly_swimlane_setup_flyout.tsx | 42 +++--- .../common/resolve_job_selection.tsx | 31 +++-- 11 files changed, 220 insertions(+), 163 deletions(-) diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 5334f420698ab..9b9ed3a93322b 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -16,6 +16,7 @@ import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { KibanaContextProvider, + KibanaThemeProvider, RedirectAppLinks, } from '../../../../../src/plugins/kibana_react/public'; import { setDependencyCache, clearCache } from './util/dependency_cache'; @@ -99,14 +100,16 @@ const App: FC = ({ coreStart, deps, appMountParams }) => { - - - + + + + + @@ -128,6 +131,7 @@ export const renderApp = ( docLinks: coreStart.docLinks!, toastNotifications: coreStart.notifications.toasts, overlays: coreStart.overlays, + theme: coreStart.theme, recentlyAccessed: coreStart.chrome!.recentlyAccessed, basePath: coreStart.http.basePath, savedObjectsClient: coreStart.savedObjects.client, diff --git a/x-pack/plugins/ml/public/application/license/expired_warning.tsx b/x-pack/plugins/ml/public/application/license/expired_warning.tsx index c8028b641cf9a..87b69fc54fc28 100644 --- a/x-pack/plugins/ml/public/application/license/expired_warning.tsx +++ b/x-pack/plugins/ml/public/application/license/expired_warning.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiCallOut } from '@elastic/eui'; -import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; -import { getOverlays } from '../util/dependency_cache'; +import { toMountPoint, wrapWithTheme } from '../../../../../../src/plugins/kibana_react/public'; +import { getOverlays, getTheme } from '../util/dependency_cache'; let expiredLicenseBannerId: string; @@ -20,8 +20,15 @@ export function showExpiredLicenseWarning() { }); // Only show the banner once with no way to dismiss it const overlays = getOverlays(); + const theme = getTheme(); + expiredLicenseBannerId = overlays.banners.add( - toMountPoint() + toMountPoint( + wrapWithTheme( + , + theme.theme$ + ) + ) ); } } diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 3a969823088f1..083982e8fccd4 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -30,6 +30,7 @@ import type { ManagementAppMountParams } from '../../../../../../../../../src/pl import { checkGetManagementMlJobsResolver } from '../../../../capabilities/check_capabilities'; import { KibanaContextProvider, + KibanaThemeProvider, RedirectAppLinks, } from '../../../../../../../../../src/plugins/kibana_react/public'; @@ -139,6 +140,7 @@ export const JobsListPage: FC<{ const tabs = useTabs(isMlEnabledInSpace, spacesApi); const [currentTabId, setCurrentTabId] = useState('anomaly-detector'); const I18nContext = coreStart.i18n.Context; + const theme$ = coreStart.theme.theme$; const check = async () => { try { @@ -219,69 +221,71 @@ export const JobsListPage: FC<{ return ( - - - - - } - description={ - - } - rightSideItems={[docsLink]} - bottomBorder - /> + + + + + + } + description={ + + } + rightSideItems={[docsLink]} + bottomBorder + /> - + - - - - {spacesEnabled && ( - <> - setShowSyncFlyout(true)} - data-test-subj="mlStackMgmtSyncButton" - > - {i18n.translate('xpack.ml.management.jobsList.syncFlyoutButton', { - defaultMessage: 'Synchronize saved objects', - })} - - {showSyncFlyout && } - - - )} - - - - - - - - - {renderTabs()} - - - - + + + + {spacesEnabled && ( + <> + setShowSyncFlyout(true)} + data-test-subj="mlStackMgmtSyncButton" + > + {i18n.translate('xpack.ml.management.jobsList.syncFlyoutButton', { + defaultMessage: 'Synchronize saved objects', + })} + + {showSyncFlyout && } + + + )} + + + + + + + + + {renderTabs()} + + + + + ); diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/force_stop_dialog.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/force_stop_dialog.tsx index 86120a4003e23..30e110317148b 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/force_stop_dialog.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/force_stop_dialog.tsx @@ -8,9 +8,9 @@ import React, { FC } from 'react'; import { EuiConfirmModal } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { OverlayStart } from 'kibana/public'; +import type { OverlayStart, ThemeServiceStart } from 'kibana/public'; import type { ModelItem } from './models_list'; -import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint, wrapWithTheme } from '../../../../../../../src/plugins/kibana_react/public'; interface ForceStopModelConfirmDialogProps { model: ModelItem; @@ -64,22 +64,25 @@ export const ForceStopModelConfirmDialog: FC = }; export const getUserConfirmationProvider = - (overlays: OverlayStart) => async (forceStopModel: ModelItem) => { + (overlays: OverlayStart, theme: ThemeServiceStart) => async (forceStopModel: ModelItem) => { return new Promise(async (resolve, reject) => { try { const modalSession = overlays.openModal( toMountPoint( - { - modalSession.close(); - resolve(false); - }} - onConfirm={() => { - modalSession.close(); - resolve(true); - }} - /> + wrapWithTheme( + { + modalSession.close(); + resolve(false); + }} + onConfirm={() => { + modalSession.close(); + resolve(true); + }} + />, + theme.theme$ + ) ) ); } catch (e) { diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx index c80ff808aa539..75659a1e3567d 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx @@ -82,6 +82,7 @@ export const ModelsList: FC = () => { services: { application: { navigateToUrl, capabilities }, overlays, + theme, }, } = useMlKibana(); const urlLocator = useMlLocator()!; @@ -112,7 +113,7 @@ export const ModelsList: FC = () => { {} ); - const getUserConfirmation = useMemo(() => getUserConfirmationProvider(overlays), []); + const getUserConfirmation = useMemo(() => getUserConfirmationProvider(overlays, theme), []); const navigateToPath = useNavigateToPath(); diff --git a/x-pack/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts index 7b6b75677dddd..93d7c069d873d 100644 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/plugins/ml/public/application/util/dependency_cache.ts @@ -16,6 +16,7 @@ import type { DocLinksStart, ToastsStart, OverlayStart, + ThemeServiceStart, ChromeRecentlyAccessed, IBasePath, } from 'kibana/public'; @@ -34,6 +35,7 @@ export interface DependencyCache { docLinks: DocLinksStart | null; toastNotifications: ToastsStart | null; overlays: OverlayStart | null; + theme: ThemeServiceStart | null; recentlyAccessed: ChromeRecentlyAccessed | null; fieldFormats: DataPublicPluginStart['fieldFormats'] | null; autocomplete: DataPublicPluginStart['autocomplete'] | null; @@ -57,6 +59,7 @@ const cache: DependencyCache = { docLinks: null, toastNotifications: null, overlays: null, + theme: null, recentlyAccessed: null, fieldFormats: null, autocomplete: null, @@ -80,6 +83,7 @@ export function setDependencyCache(deps: Partial) { cache.docLinks = deps.docLinks || null; cache.toastNotifications = deps.toastNotifications || null; cache.overlays = deps.overlays || null; + cache.theme = deps.theme || null; cache.recentlyAccessed = deps.recentlyAccessed || null; cache.fieldFormats = deps.fieldFormats || null; cache.autocomplete = deps.autocomplete || null; @@ -128,6 +132,13 @@ export function getOverlays() { return cache.overlays; } +export function getTheme() { + if (cache.theme === null) { + throw new Error("theme hasn't been initialized"); + } + return cache.theme; +} + export function getUiSettings() { if (cache.config === null) { throw new Error("uiSettings hasn't been initialized"); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx index 60b7c628229b9..ce0a270c35306 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx @@ -10,7 +10,10 @@ import ReactDOM from 'react-dom'; import { CoreStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { Subject } from 'rxjs'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { + KibanaContextProvider, + KibanaThemeProvider, +} from '../../../../../../src/plugins/kibana_react/public'; import { Embeddable, IContainer } from '../../../../../../src/plugins/embeddable/public'; import { EmbeddableAnomalyChartsContainer } from './embeddable_anomaly_charts_container_lazy'; import type { JobId } from '../../../common/types/anomaly_detection_jobs'; @@ -96,22 +99,25 @@ export class AnomalyChartsEmbeddable extends Embeddable< this.node = node; const I18nContext = this.services[0].i18n.Context; + const theme$ = this.services[0].theme.theme$; ReactDOM.render( - - }> - - - + + + }> + + + + , node ); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx index 5090274ca7383..c4ac15ffdbe76 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { CoreStart } from 'kibana/public'; import { VIEW_BY_JOB_LABEL } from '../../application/explorer/explorer_constants'; -import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint, wrapWithTheme } from '../../../../../../src/plugins/kibana_react/public'; import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; import { getDefaultExplorerChartsPanelTitle } from './anomaly_charts_embeddable'; import { HttpService } from '../../application/services/http_service'; @@ -31,24 +31,28 @@ export async function resolveEmbeddableAnomalyChartsUserInput( const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise(); const influencers = anomalyDetectorService.extractInfluencers(jobs); influencers.push(VIEW_BY_JOB_LABEL); + const { theme$ } = coreStart.theme; const modalSession = overlays.openModal( toMountPoint( - { - modalSession.close(); - resolve({ - jobIds, - title: panelTitle, - maxSeriesToPlot, - }); - }} - onCancel={() => { - modalSession.close(); - reject(); - }} - /> + wrapWithTheme( + { + modalSession.close(); + resolve({ + jobIds, + title: panelTitle, + maxSeriesToPlot, + }); + }} + onCancel={() => { + modalSession.close(); + reject(); + }} + />, + theme$ + ) ) ); } catch (error) { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx index 7f9e99f3a0c8e..e168029148006 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx @@ -10,7 +10,10 @@ import ReactDOM from 'react-dom'; import { CoreStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { Subject } from 'rxjs'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { + KibanaContextProvider, + KibanaThemeProvider, +} from '../../../../../../src/plugins/kibana_react/public'; import { Embeddable, IContainer } from '../../../../../../src/plugins/embeddable/public'; import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container_lazy'; import type { JobId } from '../../../common/types/anomaly_detection_jobs'; @@ -58,22 +61,25 @@ export class AnomalySwimlaneEmbeddable extends Embeddable< this.node = node; const I18nContext = this.services[0].i18n.Context; + const theme$ = this.services[0].theme.theme$; ReactDOM.render( - - }> - - - + + + }> + + + + , node ); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx index 5027eb6783a64..28cf197de5dfe 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { CoreStart } from 'kibana/public'; import { VIEW_BY_JOB_LABEL } from '../../application/explorer/explorer_constants'; -import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint, wrapWithTheme } from '../../../../../../src/plugins/kibana_react/public'; import { AnomalySwimlaneInitializer } from './anomaly_swimlane_initializer'; import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; import { getDefaultSwimlanePanelTitle } from './anomaly_swimlane_embeddable'; @@ -31,26 +31,30 @@ export async function resolveAnomalySwimlaneUserInput( const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise(); const influencers = anomalyDetectorService.extractInfluencers(jobs); influencers.push(VIEW_BY_JOB_LABEL); + const { theme$ } = coreStart.theme; const modalSession = overlays.openModal( toMountPoint( - { - modalSession.close(); - resolve({ - jobIds, - title: panelTitle, - swimlaneType, - viewBy, - }); - }} - onCancel={() => { - modalSession.close(); - reject(); - }} - /> + wrapWithTheme( + { + modalSession.close(); + resolve({ + jobIds, + title: panelTitle, + swimlaneType, + viewBy, + }); + }} + onCancel={() => { + modalSession.close(); + reject(); + }} + />, + theme$ + ) ) ); } catch (error) { diff --git a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx index fbceeb7f7cf79..bf7ea8eac3f50 100644 --- a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx +++ b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx @@ -13,6 +13,7 @@ import { getInitialGroupsMap } from '../../application/components/job_selector/j import { KibanaContextProvider, toMountPoint, + wrapWithTheme, } from '../../../../../../src/plugins/kibana_react/public'; import { getMlGlobalServices } from '../../application/app'; import { DashboardConstants } from '../../../../../../src/plugins/dashboard/public'; @@ -34,6 +35,7 @@ export async function resolveJobSelection( const { http, uiSettings, + theme, application: { currentAppId$ }, } = coreStart; @@ -70,18 +72,23 @@ export async function resolveJobSelection( const flyoutSession = coreStart.overlays.openFlyout( toMountPoint( - - - + wrapWithTheme( + + + , + theme.theme$ + ) ), { 'data-test-subj': 'mlFlyoutJobSelector', From da96f61330c0888180f5fa3cddc88dd16cda1afa Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Mon, 13 Dec 2021 12:16:20 +0100 Subject: [PATCH 022/145] Hosts Risk Step 2 - Hosts Page - Risk Column #119734 (#120487) * Add Host risk classification column to All hosts table * Add cypress test to risk column on all hosts table * Fix unit test * Add unit test * Add tooltip to host risk column Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../security_solution/hosts/common/index.ts | 1 + .../hosts/risk_score/index.test.ts | 14 ++ .../hosts/risk_score/index.ts | 7 +- .../integration/hosts/hosts_risk_column.ts | 33 +++ .../integration/hosts/risky_hosts_kpi.spec.ts | 5 - .../hosts_risk/use_hosts_risk_score.ts | 6 +- .../use_hosts_risk_score_complete.ts | 4 +- .../security_solution/public/helpers.test.tsx | 7 - .../security_solution/public/helpers.tsx | 5 - .../common/host_risk_score.test.tsx | 102 +++++++++ .../components/common/host_risk_score.tsx | 44 ++++ .../hosts/components/hosts_table/columns.tsx | 214 ++++++++++-------- .../components/hosts_table/index.test.tsx | 48 ++++ .../hosts/components/hosts_table/index.tsx | 11 +- .../components/hosts_table/translations.ts | 12 + .../kpi_hosts/risky_hosts/index.tsx | 38 +--- .../kpi_hosts/risky_hosts/index.tsx | 7 +- .../security_solution/server/plugin.ts | 4 +- .../factory/hosts/all/__mocks__/index.ts | 24 ++ .../factory/hosts/all/index.test.ts | 96 +++++++- .../factory/hosts/all/index.ts | 73 +++++- .../hosts/risk_score/query.hosts_risk.dsl.ts | 6 +- .../security_solution/factory/types.ts | 1 + .../security_solution/index.ts | 5 +- .../es_archives/risky_hosts/data.json | 2 +- .../es_archives/risky_hosts/mappings.json | 8 +- 26 files changed, 606 insertions(+), 171 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.test.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/hosts/hosts_risk_column.ts create mode 100644 x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.tsx diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts index f6f5ad4cd23f1..8a9a047aab3fd 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts @@ -45,6 +45,7 @@ export interface HostItem { endpoint?: Maybe; host?: Maybe; lastSeen?: Maybe; + risk?: string; } export interface HostValue { diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.test.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.test.ts new file mode 100644 index 0000000000000..8c58ccaabe8df --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.test.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getHostRiskIndex } from '.'; + +describe('hosts risk search_strategy getHostRiskIndex', () => { + it('should properly return index if space is specified', () => { + expect(getHostRiskIndex('testName')).toEqual('ml_host_risk_score_latest_testName'); + }); +}); diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts index 23cda0b68f038..4273c08c638f3 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts @@ -10,12 +10,13 @@ import type { IEsSearchRequest, IEsSearchResponse, } from '../../../../../../../../src/plugins/data/common'; +import { RISKY_HOSTS_INDEX_PREFIX } from '../../../../constants'; import { Inspect, Maybe, TimerangeInput } from '../../../common'; export interface HostsRiskScoreRequestOptions extends IEsSearchRequest { defaultIndex: string[]; factoryQueryType?: FactoryQueryTypes; - hostName?: string; + hostNames?: string[]; timerange?: TimerangeInput; } @@ -38,3 +39,7 @@ export interface RuleRisk { rule_name: string; rule_risk: string; } + +export const getHostRiskIndex = (spaceId: string): string => { + return `${RISKY_HOSTS_INDEX_PREFIX}${spaceId}`; +}; diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/hosts_risk_column.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/hosts_risk_column.ts new file mode 100644 index 0000000000000..bb57a8973c8e6 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/hosts/hosts_risk_column.ts @@ -0,0 +1,33 @@ +/* + * 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 { loginAndWaitForPage } from '../../tasks/login'; + +import { HOSTS_URL } from '../../urls/navigation'; +import { cleanKibana } from '../../tasks/common'; +import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; +import { TABLE_CELL } from '../../screens/alerts_details'; +import { kqlSearch } from '../../tasks/security_header'; + +describe('All hosts table', () => { + before(() => { + cleanKibana(); + esArchiverLoad('risky_hosts'); + }); + + after(() => { + esArchiverUnload('risky_hosts'); + }); + + it('it renders risk column', () => { + loginAndWaitForPage(HOSTS_URL); + kqlSearch('host.name: "siem-kibana" {enter}'); + + cy.get('[data-test-subj="tableHeaderCell_node.risk_4"]').should('exist'); + cy.get(`${TABLE_CELL} .euiTableCellContent`).eq(4).should('have.text', 'Low'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/risky_hosts_kpi.spec.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/risky_hosts_kpi.spec.ts index 4f282e1e69d5c..602a9118128b5 100644 --- a/x-pack/plugins/security_solution/cypress/integration/hosts/risky_hosts_kpi.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/hosts/risky_hosts_kpi.spec.ts @@ -8,13 +8,8 @@ import { loginAndWaitForPage } from '../../tasks/login'; import { HOSTS_URL } from '../../urls/navigation'; -import { cleanKibana } from '../../tasks/common'; describe('RiskyHosts KPI', () => { - before(() => { - cleanKibana(); - }); - it('it renders', () => { loginAndWaitForPage(HOSTS_URL); diff --git a/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score.ts b/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score.ts index 41fcd29191da2..debdacb570ad0 100644 --- a/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score.ts +++ b/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score.ts @@ -13,10 +13,10 @@ import { useAppToasts } from '../../hooks/use_app_toasts'; import { useKibana } from '../../lib/kibana'; import { inputsActions } from '../../store/actions'; import { isIndexNotFoundError } from '../../utils/exceptions'; -import { HostsRiskScore } from '../../../../common/search_strategy'; +import { getHostRiskIndex, HostsRiskScore } from '../../../../common/search_strategy'; + import { useHostsRiskScoreComplete } from './use_hosts_risk_score_complete'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; -import { getHostRiskIndex } from '../../../helpers'; export const QUERY_ID = 'host_risk_score'; const noop = () => {}; @@ -104,7 +104,7 @@ export const useHostsRiskScore = ({ timerange: timerange ? { to: timerange.to, from: timerange.from, interval: '' } : undefined, - hostName, + hostNames: hostName ? [hostName] : undefined, defaultIndex: [getHostRiskIndex(space.id)], }); }); diff --git a/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score_complete.ts b/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score_complete.ts index 934cb88ee0d86..6faaa3c8f08db 100644 --- a/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score_complete.ts +++ b/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score_complete.ts @@ -28,7 +28,7 @@ export const getHostsRiskScore = ({ data, defaultIndex, timerange, - hostName, + hostNames, signal, }: GetHostsRiskScoreProps): Observable => data.search.search( @@ -36,7 +36,7 @@ export const getHostsRiskScore = ({ defaultIndex, factoryQueryType: HostsQueries.hostsRiskScore, timerange, - hostName, + hostNames, }, { strategy: 'securitySolutionSearchStrategy', diff --git a/x-pack/plugins/security_solution/public/helpers.test.tsx b/x-pack/plugins/security_solution/public/helpers.test.tsx index 3475ac7c28f7a..5ba5d882c16d0 100644 --- a/x-pack/plugins/security_solution/public/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/helpers.test.tsx @@ -10,7 +10,6 @@ import { Capabilities } from '../../../../src/core/public'; import { CASES_FEATURE_ID, SERVER_APP_ID } from '../common/constants'; import { parseRoute, - getHostRiskIndex, isSubPluginAvailable, getSubPluginRoutesByCapabilities, RedirectRoute, @@ -65,12 +64,6 @@ describe('public helpers parseRoute', () => { }); }); -describe('public helpers export getHostRiskIndex', () => { - it('should properly return index if space is specified', () => { - expect(getHostRiskIndex('testName')).toEqual('ml_host_risk_score_latest_testName'); - }); -}); - describe('#getSubPluginRoutesByCapabilities', () => { const mockRender = () => null; const mockSubPlugins = { diff --git a/x-pack/plugins/security_solution/public/helpers.tsx b/x-pack/plugins/security_solution/public/helpers.tsx index d330da94e779c..09f955a53cd0a 100644 --- a/x-pack/plugins/security_solution/public/helpers.tsx +++ b/x-pack/plugins/security_solution/public/helpers.tsx @@ -17,7 +17,6 @@ import { EXCEPTIONS_PATH, RULES_PATH, UEBA_PATH, - RISKY_HOSTS_INDEX_PREFIX, SERVER_APP_ID, CASES_FEATURE_ID, OVERVIEW_PATH, @@ -164,10 +163,6 @@ export const isDetectionsPath = (pathname: string): boolean => { }); }; -export const getHostRiskIndex = (spaceId: string): string => { - return `${RISKY_HOSTS_INDEX_PREFIX}${spaceId}`; -}; - export const getSubPluginRoutesByCapabilities = ( subPlugins: StartedSubPlugins, capabilities: Capabilities diff --git a/x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.test.tsx new file mode 100644 index 0000000000000..4f70dce3c1160 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.test.tsx @@ -0,0 +1,102 @@ +/* + * 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 { render } from '@testing-library/react'; +import React from 'react'; +import { HostRiskSeverity } from '../../../../common/search_strategy'; +import { TestProviders } from '../../../common/mock'; +import { HostRiskScore } from './host_risk_score'; + +import { EuiHealth, EuiHealthProps } from '@elastic/eui'; + +import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...jest.requireActual('@elastic/eui'), + EuiHealth: jest.fn((props: EuiHealthProps) => ), + }; +}); + +describe('HostRiskScore', () => { + const context = {}; + it('renders critical severity risk score', () => { + const { container } = render( + + + + ); + + expect(container).toHaveTextContent(HostRiskSeverity.critical); + + expect(EuiHealth as jest.Mock).toHaveBeenLastCalledWith( + expect.objectContaining({ color: euiThemeVars.euiColorDanger }), + context + ); + }); + + it('renders hight severity risk score', () => { + const { container } = render( + + + + ); + + expect(container).toHaveTextContent(HostRiskSeverity.high); + + expect(EuiHealth as jest.Mock).toHaveBeenLastCalledWith( + expect.objectContaining({ color: euiThemeVars.euiColorVis9_behindText }), + context + ); + }); + + it('renders moderate severity risk score', () => { + const { container } = render( + + + + ); + + expect(container).toHaveTextContent(HostRiskSeverity.moderate); + + expect(EuiHealth as jest.Mock).toHaveBeenLastCalledWith( + expect.objectContaining({ color: euiThemeVars.euiColorWarning }), + context + ); + }); + + it('renders low severity risk score', () => { + const { container } = render( + + + + ); + + expect(container).toHaveTextContent(HostRiskSeverity.low); + + expect(EuiHealth as jest.Mock).toHaveBeenLastCalledWith( + expect.objectContaining({ color: euiThemeVars.euiColorVis0 }), + context + ); + }); + + it('renders unknown severity risk score', () => { + const { container } = render( + + + + ); + + expect(container).toHaveTextContent(HostRiskSeverity.unknown); + + expect(EuiHealth as jest.Mock).toHaveBeenLastCalledWith( + expect.objectContaining({ color: euiThemeVars.euiColorMediumShade }), + context + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.tsx b/x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.tsx new file mode 100644 index 0000000000000..94f344b54036f --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/common/host_risk_score.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiHealth, transparentize } from '@elastic/eui'; + +import styled, { css } from 'styled-components'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { HostRiskSeverity } from '../../../../common/search_strategy'; + +const HOST_RISK_SEVERITY_COLOUR = { + Unknown: euiLightVars.euiColorMediumShade, + Low: euiLightVars.euiColorVis0, + Moderate: euiLightVars.euiColorWarning, + High: euiLightVars.euiColorVis9_behindText, + Critical: euiLightVars.euiColorDanger, +}; + +const HostRiskBadge = styled.div<{ $severity: HostRiskSeverity }>` + ${({ theme, $severity }) => css` + width: fit-content; + padding-right: ${theme.eui.paddingSizes.s}; + padding-left: ${theme.eui.paddingSizes.xs}; + + ${($severity === 'Critical' || $severity === 'High') && + css` + background-color: ${transparentize(theme.eui.euiColorDanger, 0.2)}; + border-radius: 999px; // pill shaped + `}; + `} +`; + +export const HostRiskScore: React.FC<{ severity: HostRiskSeverity }> = ({ severity }) => ( + + + {severity} + + +); diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/columns.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/columns.tsx index 95f88da0a24ac..2aff7124990a6 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/columns.tsx @@ -24,102 +24,128 @@ import { import { HostsTableColumns } from './'; import * as i18n from './translations'; -import { Maybe } from '../../../../common/search_strategy'; +import { HostRiskSeverity, Maybe } from '../../../../common/search_strategy'; +import { HostRiskScore } from '../common/host_risk_score'; -export const getHostsColumns = (): HostsTableColumns => [ - { - field: 'node.host.name', - name: i18n.NAME, - truncateText: false, - mobileOptions: { show: true }, - sortable: true, - render: (hostName) => { - if (hostName != null && hostName.length > 0) { - const id = escapeDataProviderId(`hosts-table-hostName-${hostName[0]}`); - return ( - - snapshot.isDragging ? ( - - - - ) : ( - - ) - } - /> - ); - } - return getEmptyTagValue(); +export const getHostsColumns = (showRiskColumn: boolean): HostsTableColumns => { + const columns: HostsTableColumns = [ + { + field: 'node.host.name', + name: i18n.NAME, + truncateText: false, + mobileOptions: { show: true }, + sortable: true, + render: (hostName) => { + if (hostName != null && hostName.length > 0) { + const id = escapeDataProviderId(`hosts-table-hostName-${hostName[0]}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + width: '35%', }, - width: '35%', - }, - { - field: 'node.lastSeen', - name: ( - - <> - {i18n.LAST_SEEN}{' '} - - - - ), - truncateText: false, - mobileOptions: { show: true }, - sortable: true, - render: (lastSeen: Maybe | undefined) => { - if (lastSeen != null && lastSeen.length > 0) { - return ( - - ); - } - return getEmptyTagValue(); + { + field: 'node.lastSeen', + name: ( + + <> + {i18n.LAST_SEEN} + + + ), + truncateText: false, + mobileOptions: { show: true }, + sortable: true, + render: (lastSeen: Maybe | undefined) => { + if (lastSeen != null && lastSeen.length > 0) { + return ( + + ); + } + return getEmptyTagValue(); + }, }, - }, - { - field: 'node.host.os.name', - name: i18n.OS, - truncateText: false, - mobileOptions: { show: true }, - sortable: false, - render: (hostOsName) => { - if (hostOsName != null) { - return ( - - <>{hostOsName} - - ); - } - return getEmptyTagValue(); + { + field: 'node.host.os.name', + name: i18n.OS, + truncateText: false, + mobileOptions: { show: true }, + sortable: false, + render: (hostOsName) => { + if (hostOsName != null) { + return ( + + <>{hostOsName} + + ); + } + return getEmptyTagValue(); + }, }, - }, - { - field: 'node.host.os.version', - name: i18n.VERSION, - truncateText: false, - mobileOptions: { show: true }, - sortable: false, - render: (hostOsVersion) => { - if (hostOsVersion != null) { - return ( - - <>{hostOsVersion} - - ); - } - return getEmptyTagValue(); + { + field: 'node.host.os.version', + name: i18n.VERSION, + truncateText: false, + mobileOptions: { show: true }, + sortable: false, + render: (hostOsVersion) => { + if (hostOsVersion != null) { + return ( + + <>{hostOsVersion} + + ); + } + return getEmptyTagValue(); + }, }, - }, -]; + ]; + + if (showRiskColumn) { + columns.push({ + field: 'node.risk', + name: ( + + <> + {i18n.HOST_RISK} + + + ), + truncateText: false, + mobileOptions: { show: true }, + sortable: false, + render: (riskScore: HostRiskSeverity) => { + if (riskScore != null) { + return ; + } + return getEmptyTagValue(); + }, + }); + } + + return columns; +}; diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx index 413b8cda9b6ab..e30e87ffcb8fb 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx @@ -22,6 +22,8 @@ import { hostsModel } from '../../../hosts/store'; import { HostsTableType } from '../../../hosts/store/model'; import { HostsTable } from './index'; import { mockData } from './mock'; +import { render } from '@testing-library/react'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; jest.mock('../../../common/lib/kibana'); @@ -36,6 +38,8 @@ jest.mock('../../../common/components/query_bar', () => ({ jest.mock('../../../common/components/link_to'); +jest.mock('../../../common/hooks/use_experimental_features'); + describe('Hosts Table', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; @@ -69,6 +73,50 @@ describe('Hosts Table', () => { expect(wrapper.find('HostsTable')).toMatchSnapshot(); }); + test('it renders "Host Risk classfication" column when "riskyHostsEnabled" feature flag is enabled', () => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('tableHeaderCell_node.risk_4')).toBeInTheDocument(); + }); + + test("it doesn't renders 'Host Risk classfication' column when 'riskyHostsEnabled' feature flag is disabled", () => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); + + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('tableHeaderCell_node.riskScore_4')).not.toBeInTheDocument(); + }); + describe('Sorting on Table', () => { let wrapper: ReturnType; diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx index d20333d210559..dc9312b1ad4c4 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx @@ -25,9 +25,11 @@ import { HostItem, HostsSortField, HostsFields, + HostRiskSeverity, } from '../../../../common/search_strategy/security_solution/hosts'; import { Direction } from '../../../../common/search_strategy'; import { HostEcs, OsEcs } from '../../../../common/ecs/host'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; const tableType = hostsModel.HostsTableType.hosts; @@ -47,7 +49,8 @@ export type HostsTableColumns = [ Columns, Columns, Columns, - Columns + Columns, + Columns? ]; const rowItems: ItemsPerRow[] = [ @@ -124,8 +127,12 @@ const HostsTableComponent: React.FC = ({ }, [direction, sortField, type, dispatch] ); + const riskyHostsFeatureEnabled = useIsExperimentalFeatureEnabled('riskyHostsEnabled'); - const hostsColumns = useMemo(() => getHostsColumns(), []); + const hostsColumns = useMemo( + () => getHostsColumns(riskyHostsFeatureEnabled), + [riskyHostsFeatureEnabled] + ); const sorting = useMemo(() => getSorting(sortField, direction), [sortField, direction]); diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/translations.ts b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/translations.ts index 773a052dc71d0..88c01f695b940 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/translations.ts @@ -32,6 +32,14 @@ export const FIRST_LAST_SEEN_TOOLTIP = i18n.translate( } ); +export const HOST_RISK_TOOLTIP = i18n.translate( + 'xpack.securitySolution.hostsTable.hostRiskToolTip', + { + defaultMessage: + 'Host risk classifcation is determined by host risk score. Hosts classified as Critical or High are indicated as risky.', + } +); + export const OS = i18n.translate('xpack.securitySolution.hostsTable.osTitle', { defaultMessage: 'Operating system', }); @@ -40,6 +48,10 @@ export const VERSION = i18n.translate('xpack.securitySolution.hostsTable.version defaultMessage: 'Version', }); +export const HOST_RISK = i18n.translate('xpack.securitySolution.hostsTable.hostRiskTitle', { + defaultMessage: 'Host risk classification', +}); + export const ROWS_5 = i18n.translate('xpack.securitySolution.hostsTable.rows', { values: { numRows: 5 }, defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}', diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx index 1030ea4c5e65b..d5f0b16fbb7b6 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx @@ -8,19 +8,16 @@ import { EuiFlexGroup, EuiFlexItem, - EuiHealth, EuiHorizontalRule, EuiIcon, EuiPanel, EuiTitle, EuiText, - transparentize, } from '@elastic/eui'; import React from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { InspectButtonContainer, InspectButton } from '../../../../common/components/inspect'; - import { HostsKpiBaseComponentLoader } from '../common'; import * as i18n from './translations'; @@ -31,37 +28,10 @@ import { import { useInspectQuery } from '../../../../common/hooks/use_inspect_query'; import { useErrorToast } from '../../../../common/hooks/use_error_toast'; +import { HostRiskScore } from '../../common/host_risk_score'; const QUERY_ID = 'hostsKpiRiskyHostsQuery'; -const HOST_RISK_SEVERITY_COLOUR = { - Unknown: euiLightVars.euiColorMediumShade, - Low: euiLightVars.euiColorVis0, - Moderate: euiLightVars.euiColorWarning, - High: euiLightVars.euiColorVis9_behindText, - Critical: euiLightVars.euiColorDanger, -}; - -const HostRiskBadge = styled.div<{ $severity: HostRiskSeverity }>` - ${({ theme, $severity }) => css` - width: fit-content; - padding-right: ${theme.eui.paddingSizes.s}; - padding-left: ${theme.eui.paddingSizes.xs}; - - ${($severity === 'Critical' || $severity === 'High') && - css` - background-color: ${transparentize(theme.eui.euiColorDanger, 0.2)}; - border-radius: 999px; // pill shaped - `}; - `} -`; - -const HostRisk: React.FC<{ severity: HostRiskSeverity }> = ({ severity }) => ( - - {severity} - -); - const HostCount = styled(EuiText)` font-weight: bold; `; @@ -124,7 +94,7 @@ const RiskyHostsComponent: React.FC<{ - + @@ -136,7 +106,7 @@ const RiskyHostsComponent: React.FC<{ - + diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/risky_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/risky_hosts/index.tsx index cd9f01e2fd67c..d3785c842af90 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/risky_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/risky_hosts/index.tsx @@ -11,7 +11,11 @@ import { useEffect, useState } from 'react'; import { useObservable, withOptionalSignal } from '@kbn/securitysolution-hook-utils'; import { createFilter } from '../../../../common/containers/helpers'; -import { HostsKpiQueries, RequestBasicOptions } from '../../../../../common/search_strategy'; +import { + getHostRiskIndex, + HostsKpiQueries, + RequestBasicOptions, +} from '../../../../../common/search_strategy'; import { isCompleteResponse, @@ -21,7 +25,6 @@ import type { DataPublicPluginStart } from '../../../../../../../../src/plugins/ import type { HostsKpiRiskyHostsStrategyResponse } from '../../../../../common/search_strategy/security_solution/hosts/kpi/risky_hosts'; import { useKibana } from '../../../../common/lib/kibana'; import { isIndexNotFoundError } from '../../../../common/utils/exceptions'; -import { getHostRiskIndex } from '../../../../helpers'; export type RiskyHostsScoreRequestOptions = RequestBasicOptions; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index a676ca8779f6a..98d63e1917f73 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -324,8 +324,10 @@ export class Plugin implements ISecuritySolutionPlugin { const securitySolutionSearchStrategy = securitySolutionSearchStrategyProvider( depsStart.data, - endpointContext + endpointContext, + depsStart.spaces?.spacesService?.getSpaceId ); + plugins.data.search.registerSearchStrategy( 'securitySolutionSearchStrategy', securitySolutionSearchStrategy diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts index ce640f7d367d6..e039eff160241 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts @@ -5,7 +5,13 @@ * 2.0. */ +import { + KibanaRequest, + SavedObjectsClientContract, +} from '../../../../../../../../../../src/core/server'; +import { elasticsearchServiceMock } from '../../../../../../../../../../src/core/server/mocks'; import type { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; +import { allowedExperimentalValues } from '../../../../../../../common/experimental_features'; import { Direction, @@ -14,6 +20,8 @@ import { HostsQueries, HostsRequestOptions, } from '../../../../../../../common/search_strategy'; +import { EndpointAppContextService } from '../../../../../../endpoint/endpoint_app_context_services'; +import { EndpointAppContext } from '../../../../../../endpoint/types'; export const mockOptions: HostsRequestOptions = { defaultIndex: [ @@ -833,3 +841,19 @@ export const expectedDsl = { 'winlogbeat-*', ], }; + +export const mockDeps = { + esClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: {} as SavedObjectsClientContract, + endpointContext: { + logFactory: { + get: jest.fn(), + }, + config: jest.fn().mockResolvedValue({}), + experimentalFeatures: { + ...allowedExperimentalValues, + }, + service: {} as EndpointAppContextService, + } as EndpointAppContext, + request: {} as KibanaRequest, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.test.ts index 7fc43be9b800e..2739b912c42db 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.test.ts @@ -14,7 +14,30 @@ import { mockOptions, mockSearchStrategyResponse, formattedSearchStrategyResponse, + mockDeps as defaultMockDeps, } from './__mocks__'; +import { get } from 'lodash/fp'; + +class IndexNotFoundException extends Error { + meta: { body: { error: { type: string } } }; + + constructor() { + super(); + this.meta = { body: { error: { type: 'index_not_found_exception' } } }; + } +} + +const mockDeps = (riskyHostsEnabled = true) => ({ + ...defaultMockDeps, + spaceId: 'test-space', + endpointContext: { + ...defaultMockDeps.endpointContext, + experimentalFeatures: { + ...defaultMockDeps.endpointContext.experimentalFeatures, + riskyHostsEnabled, + }, + }, +}); describe('allHosts search strategy', () => { const buildAllHostsQuery = jest.spyOn(buildQuery, 'buildHostsQuery'); @@ -46,8 +69,79 @@ describe('allHosts search strategy', () => { describe('parse', () => { test('should parse data correctly', async () => { - const result = await allHosts.parse(mockOptions, mockSearchStrategyResponse); + const result = await allHosts.parse(mockOptions, mockSearchStrategyResponse, mockDeps(false)); expect(result).toMatchObject(formattedSearchStrategyResponse); }); + + test('should enhance data with risk score', async () => { + const risk = 'TEST_RISK_SCORE'; + const hostName: string = get( + 'aggregations.host_data.buckets[0].key', + mockSearchStrategyResponse.rawResponse + ); + const mockedDeps = mockDeps(); + + (mockedDeps.esClient.asCurrentUser.search as jest.Mock).mockResolvedValue({ + body: { + hits: { + hits: [ + { + _source: { + risk, + host: { + name: hostName, + }, + }, + }, + ], + }, + }, + }); + + const result = await allHosts.parse(mockOptions, mockSearchStrategyResponse, mockedDeps); + + expect(result.edges[0].node.risk).toBe(risk); + }); + + test('should not enhance data when feature flag is disabled', async () => { + const risk = 'TEST_RISK_SCORE'; + const hostName: string = get( + 'aggregations.host_data.buckets[0].key', + mockSearchStrategyResponse.rawResponse + ); + const mockedDeps = mockDeps(false); + + (mockedDeps.esClient.asCurrentUser.search as jest.Mock).mockResolvedValue({ + body: { + hits: { + hits: [ + { + _source: { + risk, + host: { + name: hostName, + }, + }, + }, + ], + }, + }, + }); + + const result = await allHosts.parse(mockOptions, mockSearchStrategyResponse, mockedDeps); + + expect(result.edges[0].node.risk).toBeUndefined(); + }); + + test("should not enhance data when index doesn't exist", async () => { + const mockedDeps = mockDeps(); + (mockedDeps.esClient.asCurrentUser.search as jest.Mock).mockImplementation(() => { + throw new IndexNotFoundException(); + }); + + const result = await allHosts.parse(mockOptions, mockSearchStrategyResponse, mockedDeps); + + expect(result.edges[0].node.risk).toBeUndefined(); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts index 987420f4bf4bd..9e6abfe49d949 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts @@ -14,13 +14,22 @@ import { HostsStrategyResponse, HostsQueries, HostsRequestOptions, + HostsRiskScore, + HostsEdges, } from '../../../../../../common/search_strategy/security_solution/hosts'; +import { getHostRiskIndex } from '../../../../../../common/search_strategy'; + import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionFactory } from '../../types'; import { buildHostsQuery } from './query.all_hosts.dsl'; import { formatHostEdgesData, HOSTS_FIELDS } from './helpers'; +import { IScopedClusterClient } from '../../../../../../../../../src/core/server'; + +import { buildHostsRiskScoreQuery } from '../risk_score/query.hosts_risk.dsl'; + import { buildHostsQueryEntities } from './query.all_hosts_entities.dsl'; +import { EndpointAppContext } from '../../../../../endpoint/types'; export const allHosts: SecuritySolutionFactory = { buildDsl: (options: HostsRequestOptions) => { @@ -31,7 +40,12 @@ export const allHosts: SecuritySolutionFactory = { }, parse: async ( options: HostsRequestOptions, - response: IEsSearchResponse + response: IEsSearchResponse, + deps?: { + esClient: IScopedClusterClient; + spaceId?: string; + endpointContext: EndpointAppContext; + } ): Promise => { const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; const totalCount = getOr(0, 'aggregations.host_count.value', response.rawResponse); @@ -48,10 +62,17 @@ export const allHosts: SecuritySolutionFactory = { }; const showMorePagesIndicator = totalCount > fakeTotalCount; + const hostNames = buckets.map(getOr('', 'key')); + + const enhancedEdges = + deps?.spaceId && deps?.endpointContext.experimentalFeatures.riskyHostsEnabled + ? await enhanceEdges(edges, hostNames, deps.spaceId, deps.esClient) + : edges; + return { ...response, inspect, - edges, + edges: enhancedEdges, totalCount, pageInfo: { activePage: activePage ?? 0, @@ -62,6 +83,54 @@ export const allHosts: SecuritySolutionFactory = { }, }; +async function enhanceEdges( + edges: HostsEdges[], + hostNames: string[], + spaceId: string, + esClient: IScopedClusterClient +): Promise { + const hostRiskData = await getHostRiskData(esClient, spaceId, hostNames); + + const hostsRiskByHostName: Record | undefined = hostRiskData?.hits.hits.reduce( + (acc, hit) => ({ + ...acc, + [hit._source?.host.name ?? '']: hit._source?.risk, + }), + {} + ); + + return hostsRiskByHostName + ? edges.map(({ node, cursor }) => ({ + node: { + ...node, + risk: hostsRiskByHostName[node._id ?? ''], + }, + cursor, + })) + : edges; +} + +async function getHostRiskData( + esClient: IScopedClusterClient, + spaceId: string, + hostNames: string[] +) { + try { + const hostRiskResponse = await esClient.asCurrentUser.search( + buildHostsRiskScoreQuery({ + defaultIndex: [getHostRiskIndex(spaceId)], + hostNames, + }) + ); + return hostRiskResponse.body; + } catch (error) { + if (error?.meta?.body?.error?.type !== 'index_not_found_exception') { + throw error; + } + return undefined; + } +} + export const allHostsEntities: SecuritySolutionFactory = { buildDsl: (options: HostsRequestOptions) => { if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risk_score/query.hosts_risk.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risk_score/query.hosts_risk.dsl.ts index 05bb496a7444e..182ad7892204f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risk_score/query.hosts_risk.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risk_score/query.hosts_risk.dsl.ts @@ -9,7 +9,7 @@ import { HostsRiskScoreRequestOptions } from '../../../../../../common/search_st export const buildHostsRiskScoreQuery = ({ timerange, - hostName, + hostNames, defaultIndex, }: HostsRiskScoreRequestOptions) => { const filter = []; @@ -26,8 +26,8 @@ export const buildHostsRiskScoreQuery = ({ }); } - if (hostName) { - filter.push({ term: { 'host.name': hostName } }); + if (hostNames) { + filter.push({ terms: { 'host.name': hostNames } }); } const dslQuery = { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts index 4fe65b7e219f3..83a6096e51abf 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts @@ -31,6 +31,7 @@ export interface SecuritySolutionFactory { savedObjectsClient: SavedObjectsClientContract; endpointContext: EndpointAppContext; request: KibanaRequest; + spaceId?: string; } ) => Promise>; } diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts index c10c63db62ee3..7786962d86083 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts @@ -20,6 +20,7 @@ import { import { securitySolutionFactory } from './factory'; import { SecuritySolutionFactory } from './factory/types'; import { EndpointAppContext } from '../../endpoint/types'; +import { KibanaRequest } from '../../../../../../src/core/server'; function isObj(req: unknown): req is Record { return typeof req === 'object' && req !== null; @@ -34,7 +35,8 @@ function assertValidRequestType( export const securitySolutionSearchStrategyProvider = ( data: PluginStart, - endpointContext: EndpointAppContext + endpointContext: EndpointAppContext, + getSpaceId?: (request: KibanaRequest) => string ): ISearchStrategy, StrategyResponseType> => { const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); @@ -60,6 +62,7 @@ export const securitySolutionSearchStrategyProvider = Date: Mon, 13 Dec 2021 05:53:15 -0600 Subject: [PATCH 023/145] [data view management] fix multiple fields w/o conflicts (#121004) * display and allow sorting on fields with multiple types that may or may not conflict * update tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../indexed_fields_table.test.tsx.snap | 4 +-- .../table/__snapshots__/table.test.tsx.snap | 7 ++++++ .../components/table/table.test.tsx | 11 +++++++- .../components/table/table.tsx | 4 ++- .../indexed_fields_table.tsx | 25 +++++++------------ .../edit_index_pattern/tabs/tabs.tsx | 16 ++++++------ 6 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap index fb463d0a5fb18..8b6e0a1682750 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap @@ -116,7 +116,7 @@ exports[`IndexedFieldsTable IndexedFieldsTable with rollup index pattern should "isUserEditable": false, "kbnType": undefined, "name": "conflictingField", - "type": "conflict", + "type": "keyword, long", }, Object { "displayName": "amount", @@ -274,7 +274,7 @@ exports[`IndexedFieldsTable should render normally 1`] = ` "isUserEditable": false, "kbnType": undefined, "name": "conflictingField", - "type": "conflict", + "type": "keyword, long", }, Object { "displayName": "amount", diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap index 100100106127b..2b6cf62baf221 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/__snapshots__/table.test.tsx.snap @@ -107,6 +107,7 @@ exports[`Table render name 2`] = ` exports[`Table should render conflicting type 1`] = ` + text, long `; +exports[`Table should render mixed, non-conflicting type 1`] = ` + + keyword, constant_keyword + +`; + exports[`Table should render normal field name 1`] = ` Elastic diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx index ec18665ccbaf3..dd78b00f9775e 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx @@ -126,7 +126,7 @@ describe('Table', () => { const tableCell = shallow( renderTable() .prop('columns')[1] - .render('conflict', { + .render('text, long', { kbnType: 'conflict', conflictDescriptions: { keyword: ['index_a'], long: ['index_b'] }, }) @@ -134,6 +134,15 @@ describe('Table', () => { expect(tableCell).toMatchSnapshot(); }); + test('should render mixed, non-conflicting type', () => { + const tableCell = shallow( + renderTable().prop('columns')[1].render('keyword, constant_keyword', { + kbnType: 'string', + }) + ); + expect(tableCell).toMatchSnapshot(); + }); + test('should allow edits', () => { const editField = jest.fn(); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx index e08b153f0b262..6a82d0380629c 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -349,9 +349,11 @@ export class Table extends PureComponent { } renderFieldType(type: string, field: IndexedFieldItem) { + const conflictDescription = + field.conflictDescriptions && field.conflictDescriptions[field.name]; return ( - {type !== 'conflict' ? type : ''} + {type === 'conflict' && conflictDescription ? '' : type} {field.conflictDescriptions ? getConflictBtn(field.name, field.conflictDescriptions, this.props.openModal) : ''} diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 1e0d36f465be5..a72c87655fd63 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -7,7 +7,6 @@ */ import React, { Component } from 'react'; -import { i18n } from '@kbn/i18n'; import { createSelector } from 'reselect'; import { OverlayStart } from 'src/core/public'; import { IndexPatternField, IndexPattern } from '../../../../../../plugins/data/public'; @@ -68,25 +67,12 @@ class IndexedFields extends Component) => f.value); const fieldWildcardMatch = fieldWildcardMatcher(sourceFilters || []); - const getDisplayEsType = (arr: string[]): string => { - const length = arr.length; - if (length < 1) { - return ''; - } - if (length > 1) { - return i18n.translate('indexPatternManagement.editIndexPattern.fields.conflictType', { - defaultMessage: 'conflict', - }); - } - return arr[0]; - }; - return ( (fields && fields.map((field) => { return { ...field.spec, - type: getDisplayEsType(field.esTypes || []), + type: field.esTypes?.join(', ') || '', kbnType: field.type, displayName: field.displayName, format: indexPattern.getFormatterForFieldNoDefault(field.name)?.type?.title || '', @@ -117,7 +103,14 @@ class IndexedFields extends Component field.type === indexedFieldTypeFilter); + // match conflict fields + fields = fields.filter((field) => { + if (indexedFieldTypeFilter === 'conflict' && field.kbnType === 'conflict') { + return true; + } + // match one of multiple types on a field + return field.esTypes?.length && field.esTypes?.indexOf(indexedFieldTypeFilter) !== -1; + }); } return fields; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx index c79871dbc8d71..b5940fa8d1bb0 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -93,13 +93,6 @@ export function Tabs({ const closeEditorHandler = useRef<() => void | undefined>(); const { DeleteRuntimeFieldProvider } = dataViewFieldEditor; - const conflict = i18n.translate( - 'indexPatternManagement.editIndexPattern.fieldTypes.conflictType', - { - defaultMessage: 'conflict', - } - ); - const refreshFilters = useCallback(() => { const tempIndexedFieldTypes: string[] = []; const tempScriptedFieldLanguages: string[] = []; @@ -109,8 +102,13 @@ export function Tabs({ tempScriptedFieldLanguages.push(field.lang); } } else { + // for conflicted fields, add conflict as a type + if (field.type === 'conflict') { + tempIndexedFieldTypes.push('conflict'); + } if (field.esTypes) { - tempIndexedFieldTypes.push(field.esTypes.length === 1 ? field.esTypes[0] : conflict); + // add all types, may be multiple + field.esTypes.forEach((item) => tempIndexedFieldTypes.push(item)); } } }); @@ -119,7 +117,7 @@ export function Tabs({ setScriptedFieldLanguages( convertToEuiSelectOption(tempScriptedFieldLanguages, 'scriptedFieldLanguages') ); - }, [indexPattern, conflict]); + }, [indexPattern]); const closeFieldEditor = useCallback(() => { if (closeEditorHandler.current) { From 31946d5a477a307493d58ddf1fdfa837e5a8253b Mon Sep 17 00:00:00 2001 From: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com> Date: Mon, 13 Dec 2021 13:43:37 +0100 Subject: [PATCH 024/145] [RAC][Refactoring] Rename alerting types in Infra (#121061) --- .../alerting/logs/log_threshold/types.ts | 67 +++++++++---------- .../log_threshold/components/alert_flyout.tsx | 4 +- .../components/expression_editor/criteria.tsx | 14 ++-- .../criterion_preview_chart.tsx | 28 ++++---- .../components/expression_editor/editor.tsx | 30 ++++----- .../expression_editor/threshold.tsx | 4 +- .../expression_editor/type_switcher.tsx | 4 +- .../log_threshold/log_threshold_rule_type.ts | 8 +-- .../alerting/log_threshold/validation.ts | 8 +-- x-pack/plugins/infra/server/features.ts | 12 ++-- .../log_threshold_executor.test.ts | 52 +++++++------- .../log_threshold/log_threshold_executor.ts | 44 ++++++------ .../register_log_threshold_rule_type.ts | 8 +-- 13 files changed, 140 insertions(+), 143 deletions(-) diff --git a/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts index 6da0bb58e4e85..dba94d2c8fd93 100644 --- a/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts +++ b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import * as rt from 'io-ts'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; -export const LOG_DOCUMENT_COUNT_ALERT_TYPE_ID = 'logs.alert.document.count'; +export const LOG_DOCUMENT_COUNT_RULE_TYPE_ID = 'logs.alert.document.count'; const ThresholdTypeRT = rt.keyof({ count: null, @@ -143,7 +143,7 @@ export type TimeUnit = rt.TypeOf; export const timeSizeRT = rt.number; export const groupByRT = rt.array(rt.string); -const RequiredAlertParamsRT = rt.type({ +const RequiredRuleParamsRT = rt.type({ // NOTE: "count" would be better named as "threshold", but this would require a // migration of encrypted saved objects, so we'll keep "count" until it's problematic. count: ThresholdRT, @@ -151,72 +151,69 @@ const RequiredAlertParamsRT = rt.type({ timeSize: timeSizeRT, }); -const partialRequiredAlertParamsRT = rt.partial(RequiredAlertParamsRT.props); -export type PartialRequiredAlertParams = rt.TypeOf; +const partialRequiredRuleParamsRT = rt.partial(RequiredRuleParamsRT.props); +export type PartialRequiredRuleParams = rt.TypeOf; -const OptionalAlertParamsRT = rt.partial({ +const OptionalRuleParamsRT = rt.partial({ groupBy: groupByRT, }); -export const countAlertParamsRT = rt.intersection([ +export const countRuleParamsRT = rt.intersection([ rt.type({ criteria: countCriteriaRT, - ...RequiredAlertParamsRT.props, + ...RequiredRuleParamsRT.props, }), rt.partial({ - ...OptionalAlertParamsRT.props, + ...OptionalRuleParamsRT.props, }), ]); -export type CountAlertParams = rt.TypeOf; +export type CountRuleParams = rt.TypeOf; -export const partialCountAlertParamsRT = rt.intersection([ +export const partialCountRuleParamsRT = rt.intersection([ rt.type({ criteria: partialCountCriteriaRT, - ...RequiredAlertParamsRT.props, + ...RequiredRuleParamsRT.props, }), rt.partial({ - ...OptionalAlertParamsRT.props, + ...OptionalRuleParamsRT.props, }), ]); -export type PartialCountAlertParams = rt.TypeOf; +export type PartialCountRuleParams = rt.TypeOf; -export const ratioAlertParamsRT = rt.intersection([ +export const ratioRuleParamsRT = rt.intersection([ rt.type({ criteria: ratioCriteriaRT, - ...RequiredAlertParamsRT.props, + ...RequiredRuleParamsRT.props, }), rt.partial({ - ...OptionalAlertParamsRT.props, + ...OptionalRuleParamsRT.props, }), ]); -export type RatioAlertParams = rt.TypeOf; +export type RatioRuleParams = rt.TypeOf; -export const partialRatioAlertParamsRT = rt.intersection([ +export const partialRatioRuleParamsRT = rt.intersection([ rt.type({ criteria: partialRatioCriteriaRT, - ...RequiredAlertParamsRT.props, + ...RequiredRuleParamsRT.props, }), rt.partial({ - ...OptionalAlertParamsRT.props, + ...OptionalRuleParamsRT.props, }), ]); -export type PartialRatioAlertParams = rt.TypeOf; +export type PartialRatioRuleParams = rt.TypeOf; -export const alertParamsRT = rt.union([countAlertParamsRT, ratioAlertParamsRT]); -export type AlertParams = rt.TypeOf; +export const ruleParamsRT = rt.union([countRuleParamsRT, ratioRuleParamsRT]); +export type RuleParams = rt.TypeOf; -export const partialAlertParamsRT = rt.union([ - partialCountAlertParamsRT, - partialRatioAlertParamsRT, -]); -export type PartialAlertParams = rt.TypeOf; +export const partialRuleParamsRT = rt.union([partialCountRuleParamsRT, partialRatioRuleParamsRT]); +export type PartialRuleParams = rt.TypeOf; -export const isRatioAlert = (criteria: PartialCriteria): criteria is PartialRatioCriteria => { +export const isRatioRule = (criteria: PartialCriteria): criteria is PartialRatioCriteria => { return criteria.length > 0 && Array.isArray(criteria[0]) ? true : false; }; -export const isRatioAlertParams = (params: AlertParams): params is RatioAlertParams => { - return isRatioAlert(params.criteria); +export const isRatioRuleParams = (params: RuleParams): params is RatioRuleParams => { + return isRatioRule(params.criteria); }; export const getNumerator = (criteria: C): C[0] => { @@ -229,8 +226,8 @@ export const getDenominator = ( return criteria[1]; }; -export const hasGroupBy = (alertParams: AlertParams) => { - const { groupBy } = alertParams; +export const hasGroupBy = (params: RuleParams) => { + const { groupBy } = params; return groupBy && groupBy.length > 0 ? true : false; }; @@ -339,8 +336,8 @@ export const isOptimizedGroupedSearchQueryResponse = ( }; export const isOptimizableGroupedThreshold = ( - selectedComparator: AlertParams['count']['comparator'], - selectedValue?: AlertParams['count']['value'] + selectedComparator: RuleParams['count']['comparator'], + selectedValue?: RuleParams['count']['value'] ) => { if (selectedComparator === Comparator.GT) { return true; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx index 346b44218b612..bee7f93a538be 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_flyout.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useContext, useMemo } from 'react'; import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../common/alerting/logs/log_threshold/types'; +import { LOG_DOCUMENT_COUNT_RULE_TYPE_ID } from '../../../../common/alerting/logs/log_threshold/types'; interface Props { visible?: boolean; @@ -25,7 +25,7 @@ export const AlertFlyout = (props: Props) => { consumer: 'logs', onClose: onCloseFlyout, canChangeTrigger: false, - alertTypeId: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + alertTypeId: LOG_DOCUMENT_COUNT_RULE_TYPE_ID, metadata: { isInternal: true, }, diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx index 9ff9b602fac3b..1be120210984f 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx @@ -12,12 +12,12 @@ import { i18n } from '@kbn/i18n'; import { IFieldType } from 'src/plugins/data/public'; import { Criterion } from './criterion'; import { - PartialAlertParams, + PartialRuleParams, PartialCountCriteria as PartialCountCriteriaType, PartialCriteria as PartialCriteriaType, PartialCriterion as PartialCriterionType, PartialRatioCriteria as PartialRatioCriteriaType, - isRatioAlert, + isRatioRule, getNumerator, getDenominator, } from '../../../../../common/alerting/logs/log_threshold/types'; @@ -38,7 +38,7 @@ interface SharedProps { criteria?: PartialCriteriaType; defaultCriterion: PartialCriterionType; errors: Errors['criteria']; - alertParams: PartialAlertParams; + ruleParams: PartialRuleParams; sourceId: string; updateCriteria: (criteria: PartialCriteriaType) => void; } @@ -49,7 +49,7 @@ export const Criteria: React.FC = (props) => { const { criteria, errors } = props; if (!criteria || criteria.length === 0) return null; - return !isRatioAlert(criteria) ? ( + return !isRatioRule(criteria) ? ( ) : ( @@ -57,7 +57,7 @@ export const Criteria: React.FC = (props) => { }; interface CriteriaWrapperProps { - alertParams: SharedProps['alertParams']; + ruleParams: SharedProps['ruleParams']; fields: SharedProps['fields']; updateCriterion: (idx: number, params: PartialCriterionType) => void; removeCriterion: (idx: number) => void; @@ -76,7 +76,7 @@ const CriteriaWrapper: React.FC = (props) => { criteria, fields, errors, - alertParams, + ruleParams, sourceId, isRatio = false, } = props; @@ -103,7 +103,7 @@ const CriteriaWrapper: React.FC = (props) => { arrowDisplay="right" > ; sourceId: string; showThreshold: boolean; } export const CriterionPreview: React.FC = ({ - alertParams, + ruleParams, chartCriterion, sourceId, showThreshold, @@ -69,12 +69,12 @@ export const CriterionPreview: React.FC = ({ const params = { criteria, count: { - comparator: alertParams.count.comparator, - value: alertParams.count.value, + comparator: ruleParams.count.comparator, + value: ruleParams.count.value, }, - timeSize: alertParams.timeSize, - timeUnit: alertParams.timeUnit, - groupBy: alertParams.groupBy, + timeSize: ruleParams.timeSize, + timeUnit: ruleParams.timeUnit, + groupBy: ruleParams.groupBy, }; try { @@ -83,11 +83,11 @@ export const CriterionPreview: React.FC = ({ return null; } }, [ - alertParams.timeSize, - alertParams.timeUnit, - alertParams.groupBy, - alertParams.count.comparator, - alertParams.count.value, + ruleParams.timeSize, + ruleParams.timeUnit, + ruleParams.groupBy, + ruleParams.count.comparator, + ruleParams.count.value, chartCriterion, ]); @@ -102,7 +102,7 @@ export const CriterionPreview: React.FC = ({ : NUM_BUCKETS / 4 } // Display less data for groups due to space limitations sourceId={sourceId} - threshold={alertParams.count} + threshold={ruleParams.count} chartAlertParams={chartAlertParams} showThreshold={showThreshold} /> diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx index 02b827d5915dd..312662286595c 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx @@ -16,11 +16,11 @@ import { } from '../../../../../../triggers_actions_ui/public'; import { Comparator, - isRatioAlert, - PartialAlertParams, - PartialCountAlertParams, + isRatioRule, + PartialRuleParams, + PartialCountRuleParams, PartialCriteria as PartialCriteriaType, - PartialRatioAlertParams, + PartialRatioRuleParams, ThresholdType, timeUnitRT, isOptimizableGroupedThreshold, @@ -64,9 +64,9 @@ const createDefaultCriterion = ( ? { field: DEFAULT_FIELD, comparator: Comparator.EQ, value } : { field: undefined, comparator: undefined, value: undefined }; -const createDefaultCountAlertParams = ( +const createDefaultCountRuleParams = ( availableFields: LogIndexField[] -): PartialCountAlertParams => ({ +): PartialCountRuleParams => ({ ...DEFAULT_BASE_EXPRESSION, count: { value: 75, @@ -75,9 +75,9 @@ const createDefaultCountAlertParams = ( criteria: [createDefaultCriterion(availableFields, 'error')], }); -const createDefaultRatioAlertParams = ( +const createDefaultRatioRuleParams = ( availableFields: LogIndexField[] -): PartialRatioAlertParams => ({ +): PartialRatioRuleParams => ({ ...DEFAULT_BASE_EXPRESSION, count: { value: 2, @@ -90,7 +90,7 @@ const createDefaultRatioAlertParams = ( }); export const ExpressionEditor: React.FC< - AlertTypeParamsExpressionProps + AlertTypeParamsExpressionProps > = (props) => { const isInternal = props.metadata?.isInternal ?? false; const [sourceId] = useSourceId(); @@ -159,7 +159,7 @@ export const SourceStatusWrapper: React.FC = ({ children }) => { ); }; -export const Editor: React.FC> = +export const Editor: React.FC> = (props) => { const { setAlertParams, alertParams, errors } = props; const [hasSetDefaults, setHasSetDefaults] = useState(false); @@ -231,7 +231,7 @@ export const Editor: React.FC createDefaultCountAlertParams(supportedFields), + () => createDefaultCountRuleParams(supportedFields), [supportedFields] ); @@ -240,7 +240,7 @@ export const Editor: React.FC @@ -286,7 +286,7 @@ export const Editor: React.FC - {alertParams.criteria && !isRatioAlert(alertParams.criteria) && criteriaComponent} + {alertParams.criteria && !isRatioRule(alertParams.criteria) && criteriaComponent} - {alertParams.criteria && isRatioAlert(alertParams.criteria) && criteriaComponent} + {alertParams.criteria && isRatioRule(alertParams.criteria) && criteriaComponent} {shouldShowGroupByOptimizationWarning && ( <> diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/threshold.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/threshold.tsx index af4f5dc1c8115..fdc60daceb715 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/threshold.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/threshold.tsx @@ -23,7 +23,7 @@ import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types import { Comparator, ComparatorToi18nMap, - AlertParams, + RuleParams, } from '../../../../../common/alerting/logs/log_threshold/types'; const thresholdPrefix = i18n.translate('xpack.infra.logs.alertFlyout.thresholdPrefix', { @@ -49,7 +49,7 @@ const getComparatorOptions = (): Array<{ interface Props { comparator?: Comparator; value?: number; - updateThreshold: (params: Partial) => void; + updateThreshold: (params: Partial) => void; errors: IErrorObject; } diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/type_switcher.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/type_switcher.tsx index cde97dad20613..46e865d3acbe0 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/type_switcher.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/type_switcher.tsx @@ -11,7 +11,7 @@ import { EuiFlexItem, EuiFlexGroup, EuiPopover, EuiSelect, EuiExpression } from import { PartialCriteria, ThresholdType, - isRatioAlert, + isRatioRule, } from '../../../../../common/alerting/logs/log_threshold/types'; import { ExpressionLike } from './editor'; @@ -51,7 +51,7 @@ interface Props { } const getThresholdType = (criteria: PartialCriteria): ThresholdType => { - return isRatioAlert(criteria) ? 'ratio' : 'count'; + return isRatioRule(criteria) ? 'ratio' : 'count'; }; export const TypeSwitcher: React.FC = ({ criteria, updateType }) => { diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.ts b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.ts index b0a8737a994a1..56c28074bd5fb 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.ts @@ -9,15 +9,15 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { ObservabilityRuleTypeModel } from '../../../../observability/public'; import { - LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, - PartialAlertParams, + LOG_DOCUMENT_COUNT_RULE_TYPE_ID, + PartialRuleParams, } from '../../../common/alerting/logs/log_threshold'; import { formatRuleData } from './rule_data_formatters'; import { validateExpression } from './validation'; -export function createLogThresholdRuleType(): ObservabilityRuleTypeModel { +export function createLogThresholdRuleType(): ObservabilityRuleTypeModel { return { - id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + id: LOG_DOCUMENT_COUNT_RULE_TYPE_ID, description: i18n.translate('xpack.infra.logs.alertFlyout.alertDescription', { defaultMessage: 'Alert when the log aggregation exceeds the threshold.', }), diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/validation.ts b/x-pack/plugins/infra/public/alerting/log_threshold/validation.ts index 41c07ee79344b..740805006785b 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/validation.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/validation.ts @@ -11,10 +11,10 @@ import { isNumber, isFinite } from 'lodash'; import { IErrorObject, ValidationResult } from '../../../../triggers_actions_ui/public'; import { PartialCountCriteria, - isRatioAlert, + isRatioRule, getNumerator, getDenominator, - PartialRequiredAlertParams, + PartialRequiredRuleParams, PartialCriteria, } from '../../../common/alerting/logs/log_threshold/types'; @@ -50,7 +50,7 @@ export function validateExpression({ count, criteria, timeSize, -}: PartialRequiredAlertParams & { +}: PartialRequiredRuleParams & { criteria: PartialCriteria; }): ValidationResult { const validationResult = { errors: {} }; @@ -122,7 +122,7 @@ export function validateExpression({ return _errors; }; - if (!isRatioAlert(criteria)) { + if (!isRatioRule(criteria)) { const criteriaErrors = getCriterionErrors(criteria); errors.criteria[0] = criteriaErrors; } else { diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index fe0570c4950f8..361565c3672c5 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../common/alerting/logs/log_threshold/types'; +import { LOG_DOCUMENT_COUNT_RULE_TYPE_ID } from '../common/alerting/logs/log_threshold/types'; import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/inventory_metric_threshold/types'; import { METRIC_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/metric_threshold/types'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; @@ -83,7 +83,7 @@ export const LOGS_FEATURE = { management: { insightsAndAlerting: ['triggersActions'], }, - alerting: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + alerting: [LOG_DOCUMENT_COUNT_RULE_TYPE_ID], privileges: { all: { app: ['infra', 'logs', 'kibana'], @@ -95,10 +95,10 @@ export const LOGS_FEATURE = { }, alerting: { rule: { - all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + all: [LOG_DOCUMENT_COUNT_RULE_TYPE_ID], }, alert: { - all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + all: [LOG_DOCUMENT_COUNT_RULE_TYPE_ID], }, }, management: { @@ -112,10 +112,10 @@ export const LOGS_FEATURE = { api: ['infra'], alerting: { rule: { - read: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + read: [LOG_DOCUMENT_COUNT_RULE_TYPE_ID], }, alert: { - read: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + read: [LOG_DOCUMENT_COUNT_RULE_TYPE_ID], }, }, management: { diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index b5cf05512b353..90f9c508e1038 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -18,7 +18,7 @@ import { import { Comparator, AlertStates, - AlertParams, + RuleParams, Criterion, UngroupedSearchQueryResponse, GroupedSearchQueryResponse, @@ -126,7 +126,7 @@ const expectedNegativeFilterClauses = [ }, ]; -const baseAlertParams: Pick = { +const baseRuleParams: Pick = { count: { comparator: Comparator.GT, value: 5, @@ -165,27 +165,27 @@ describe('Log threshold executor', () => { }); describe('Criteria filter building', () => { test('Handles positive criteria', () => { - const alertParams: AlertParams = { - ...baseAlertParams, + const ruleParams: RuleParams = { + ...baseRuleParams, criteria: positiveCriteria, }; - const filters = buildFiltersFromCriteria(alertParams, TIMESTAMP_FIELD); + const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD); expect(filters.mustFilters).toEqual(expectedPositiveFilterClauses); }); test('Handles negative criteria', () => { - const alertParams: AlertParams = { - ...baseAlertParams, + const ruleParams: RuleParams = { + ...baseRuleParams, criteria: negativeCriteria, }; - const filters = buildFiltersFromCriteria(alertParams, TIMESTAMP_FIELD); + const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD); expect(filters.mustNotFilters).toEqual(expectedNegativeFilterClauses); }); test('Handles time range', () => { - const alertParams: AlertParams = { ...baseAlertParams, criteria: [] }; - const filters = buildFiltersFromCriteria(alertParams, TIMESTAMP_FIELD); + const ruleParams: RuleParams = { ...baseRuleParams, criteria: [] }; + const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD); expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].gte).toBe('number'); expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].lte).toBe('number'); expect(filters.rangeFilter.range[TIMESTAMP_FIELD].format).toBe('epoch_millis'); @@ -199,12 +199,12 @@ describe('Log threshold executor', () => { describe('ES queries', () => { describe('Query generation', () => { it('Correctly generates ungrouped queries', () => { - const alertParams: AlertParams = { - ...baseAlertParams, + const ruleParams: RuleParams = { + ...baseRuleParams, criteria: [...positiveCriteria, ...negativeCriteria], }; const query = getUngroupedESQuery( - alertParams, + ruleParams, TIMESTAMP_FIELD, FILEBEAT_INDEX, runtimeMappings @@ -248,13 +248,13 @@ describe('Log threshold executor', () => { describe('Correctly generates grouped queries', () => { it('When using an optimizable threshold comparator', () => { - const alertParams: AlertParams = { - ...baseAlertParams, + const ruleParams: RuleParams = { + ...baseRuleParams, groupBy: ['host.name'], criteria: [...positiveCriteria, ...negativeCriteria], }; const query = getGroupedESQuery( - alertParams, + ruleParams, TIMESTAMP_FIELD, FILEBEAT_INDEX, runtimeMappings @@ -313,10 +313,10 @@ describe('Log threshold executor', () => { }); it('When not using an optimizable threshold comparator', () => { - const alertParams: AlertParams = { - ...baseAlertParams, + const ruleParams: RuleParams = { + ...baseRuleParams, count: { - ...baseAlertParams.count, + ...baseRuleParams.count, comparator: Comparator.LT, }, groupBy: ['host.name'], @@ -324,7 +324,7 @@ describe('Log threshold executor', () => { }; const query = getGroupedESQuery( - alertParams, + ruleParams, TIMESTAMP_FIELD, FILEBEAT_INDEX, runtimeMappings @@ -408,8 +408,8 @@ describe('Log threshold executor', () => { describe('Can process ungrouped results', () => { test('It handles the ALERT state correctly', () => { const alertUpdaterMock = jest.fn(); - const alertParams = { - ...baseAlertParams, + const ruleParams = { + ...baseRuleParams, criteria: [positiveCriteria[0]], }; const results = { @@ -421,7 +421,7 @@ describe('Log threshold executor', () => { } as UngroupedSearchQueryResponse; processUngroupedResults( results, - alertParams, + ruleParams, alertsMock.createAlertInstanceFactory, alertUpdaterMock ); @@ -445,8 +445,8 @@ describe('Log threshold executor', () => { describe('Can process grouped results', () => { test('It handles the ALERT state correctly', () => { const alertUpdaterMock = jest.fn(); - const alertParams = { - ...baseAlertParams, + const ruleParams = { + ...baseRuleParams, criteria: [positiveCriteria[0]], groupBy: ['host.name', 'event.dataset'], }; @@ -485,7 +485,7 @@ describe('Log threshold executor', () => { ] as GroupedSearchQueryResponse['aggregations']['groups']['buckets']; processGroupByResults( results, - alertParams, + ruleParams, alertsMock.createAlertInstanceFactory, alertUpdaterMock ); diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index a41c70f5c2869..daf9b486da9a0 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -22,11 +22,11 @@ import { AlertTypeState as RuleTypeState, } from '../../../../../alerting/server'; import { - AlertParams, - alertParamsRT, + RuleParams, + ruleParamsRT, AlertStates, Comparator, - CountAlertParams, + CountRuleParams, CountCriteria, Criterion, getDenominator, @@ -36,8 +36,8 @@ import { hasGroupBy, isOptimizableGroupedThreshold, isOptimizedGroupedSearchQueryResponse, - isRatioAlertParams, - RatioAlertParams, + isRatioRuleParams, + RatioRuleParams, UngroupedSearchQueryResponse, UngroupedSearchQueryResponseRT, } from '../../../../common/alerting/logs/log_threshold'; @@ -54,7 +54,7 @@ import { } from './reason_formatters'; export type LogThresholdActionGroups = ActionGroupIdsOf; -export type LogThresholdRuleTypeParams = AlertParams; +export type LogThresholdRuleTypeParams = RuleParams; export type LogThresholdRuleTypeState = RuleTypeState; // no specific state used export type LogThresholdAlertState = AlertState; // no specific state used export type LogThresholdAlertContext = AlertContext; // no specific instance context used @@ -116,9 +116,9 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => ); try { - const validatedParams = decodeOrThrow(alertParamsRT)(params); + const validatedParams = decodeOrThrow(ruleParamsRT)(params); - if (!isRatioAlertParams(validatedParams)) { + if (!isRatioRuleParams(validatedParams)) { await executeAlert( validatedParams, timestampField, @@ -143,7 +143,7 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => }); async function executeAlert( - alertParams: CountAlertParams, + alertParams: CountRuleParams, timestampField: string, indexPattern: string, runtimeMappings: estypes.MappingRuntimeFields, @@ -174,7 +174,7 @@ async function executeAlert( } async function executeRatioAlert( - alertParams: RatioAlertParams, + alertParams: RatioRuleParams, timestampField: string, indexPattern: string, runtimeMappings: estypes.MappingRuntimeFields, @@ -182,12 +182,12 @@ async function executeRatioAlert( alertFactory: LogThresholdAlertFactory ) { // Ratio alert params are separated out into two standard sets of alert params - const numeratorParams: AlertParams = { + const numeratorParams: RuleParams = { ...alertParams, criteria: getNumerator(alertParams.criteria), }; - const denominatorParams: AlertParams = { + const denominatorParams: RuleParams = { ...alertParams, criteria: getDenominator(alertParams.criteria), }; @@ -228,7 +228,7 @@ async function executeRatioAlert( } const getESQuery = ( - alertParams: Omit & { criteria: CountCriteria }, + alertParams: Omit & { criteria: CountCriteria }, timestampField: string, indexPattern: string, runtimeMappings: estypes.MappingRuntimeFields @@ -240,7 +240,7 @@ const getESQuery = ( export const processUngroupedResults = ( results: UngroupedSearchQueryResponse, - params: CountAlertParams, + params: CountRuleParams, alertFactory: LogThresholdAlertFactory, alertUpdater: AlertUpdater ) => { @@ -271,7 +271,7 @@ export const processUngroupedResults = ( export const processUngroupedRatioResults = ( numeratorResults: UngroupedSearchQueryResponse, denominatorResults: UngroupedSearchQueryResponse, - params: RatioAlertParams, + params: RatioRuleParams, alertFactory: LogThresholdAlertFactory, alertUpdater: AlertUpdater ) => { @@ -344,7 +344,7 @@ const getReducedGroupByResults = ( export const processGroupByResults = ( results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], - params: CountAlertParams, + params: CountRuleParams, alertFactory: LogThresholdAlertFactory, alertUpdater: AlertUpdater ) => { @@ -385,7 +385,7 @@ export const processGroupByResults = ( export const processGroupByRatioResults = ( numeratorResults: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], denominatorResults: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], - params: RatioAlertParams, + params: RatioRuleParams, alertFactory: LogThresholdAlertFactory, alertUpdater: AlertUpdater ) => { @@ -457,7 +457,7 @@ export const updateAlert: AlertUpdater = (alert, state, actions) => { }; export const buildFiltersFromCriteria = ( - params: Pick & { criteria: CountCriteria }, + params: Pick & { criteria: CountCriteria }, timestampField: string ) => { const { timeSize, timeUnit, criteria } = params; @@ -508,11 +508,11 @@ export const buildFiltersFromCriteria = ( }; export const getGroupedESQuery = ( - params: Pick & { + params: Pick & { criteria: CountCriteria; count: { - comparator: AlertParams['count']['comparator']; - value?: AlertParams['count']['value']; + comparator: RuleParams['count']['comparator']; + value?: RuleParams['count']['value']; }; }, timestampField: string, @@ -619,7 +619,7 @@ export const getGroupedESQuery = ( }; export const getUngroupedESQuery = ( - params: Pick & { criteria: CountCriteria }, + params: Pick & { criteria: CountCriteria }, timestampField: string, index: string, runtimeMappings: estypes.MappingRuntimeFields diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_rule_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_rule_type.ts index 05dc2682fc3b7..9a2902bfb8cd4 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_rule_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_rule_type.ts @@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n'; import { PluginSetupContract } from '../../../../../alerting/server'; import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor'; import { - LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, - alertParamsRT, + LOG_DOCUMENT_COUNT_RULE_TYPE_ID, + ruleParamsRT, } from '../../../../common/alerting/logs/log_threshold'; import { InfraBackendLibs } from '../../infra_types'; import { decodeOrThrow } from '../../../../common/runtime_types'; @@ -82,13 +82,13 @@ export async function registerLogThresholdRuleType( } alertingPlugin.registerType({ - id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + id: LOG_DOCUMENT_COUNT_RULE_TYPE_ID, name: i18n.translate('xpack.infra.logs.alertName', { defaultMessage: 'Log threshold', }), validate: { params: { - validate: (params) => decodeOrThrow(alertParamsRT)(params), + validate: (params) => decodeOrThrow(ruleParamsRT)(params), }, }, defaultActionGroupId: FIRED_ACTIONS.id, From b58445da8e36d3946fc7d74f5d8e98e16ac505be Mon Sep 17 00:00:00 2001 From: vladpro25 <91911546+vladpro25@users.noreply.github.com> Date: Mon, 13 Dec 2021 15:24:46 +0200 Subject: [PATCH 025/145] Dev Tools Console: Expose the error_trace parameter for completion (#120290) * Expose the error_trace parameter for completion * Dev Tools Console: Expose the error_trace parameter for completion * Dev Tools Console: Expose the error_trace parameter for completion Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/spec_definitions/json/overrides/search.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/plugins/console/server/lib/spec_definitions/json/overrides/search.json diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/search.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/search.json new file mode 100644 index 0000000000000..1028422b303f2 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/search.json @@ -0,0 +1,7 @@ +{ + "search": { + "url_params": { + "error_trace": true + } + } +} From e582c96f849cdb5c0c4a064f794328795dc77e8f Mon Sep 17 00:00:00 2001 From: vladpro25 <91911546+vladpro25@users.noreply.github.com> Date: Mon, 13 Dec 2021 15:25:09 +0200 Subject: [PATCH 026/145] Change suggestions for Sampler and Diversified sampler aggregations (#119355) * Change suggestions for Sampler and Diversified sampler aggregations * Change suggestions for Sampler and Diversified sampler aggregations * Change suggestions for Sampler and Diversified sampler aggregations Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../lib/spec_definitions/js/aggregations.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/plugins/console/server/lib/spec_definitions/js/aggregations.ts b/src/plugins/console/server/lib/spec_definitions/js/aggregations.ts index fc9e66e17685c..9403d6d56e486 100644 --- a/src/plugins/console/server/lib/spec_definitions/js/aggregations.ts +++ b/src/plugins/console/server/lib/spec_definitions/js/aggregations.ts @@ -111,8 +111,13 @@ const rules = { filters: {}, }, diversified_sampler: { - shard_size: '', - field: '', + shard_size: 100, + field: '{field}', + max_docs_per_value: 1, + execution_hint: { + __template: 'global_ordinals', + __one_of: ['global_ordinals', 'map', 'bytes_hash'], + }, }, min: simple_metric, max: simple_metric, @@ -446,13 +451,7 @@ const rules = { }, sampler: { __template: {}, - field: '{field}', - script: { - // populated by a global rule - }, shard_size: 100, - max_docs_per_value: 3, - execution_hint: { __one_of: ['map', 'global_ordinals', 'bytes_hash'] }, }, children: { __template: { From d916a37cdfb29372e36ea36f0e9de767d72b0428 Mon Sep 17 00:00:00 2001 From: Tobias Stadler Date: Mon, 13 Dec 2021 14:26:58 +0100 Subject: [PATCH 027/145] [APM] Prefer host.name over host.hostname (#119737) (#119952) --- .../src/lib/apm/apm_fields.ts | 3 + .../src/lib/apm/service.ts | 1 + .../test/scenarios/01_simple_trace.test.ts | 2 + .../01_simple_trace.test.ts.snap | 30 ++++++ .../elasticsearch_fieldnames.test.ts.snap | 16 +-- .../apm/common/elasticsearch_fieldnames.ts | 4 +- .../components/app/service_logs/index.tsx | 4 +- .../__snapshots__/queries.test.ts.snap | 6 +- .../routes/service_nodes/get_service_nodes.ts | 2 +- .../services/get_service_infrastructure.ts | 4 +- .../constants/elasticsearch_fieldnames.ts | 2 +- .../service_nodes/get_service_nodes.spec.ts | 98 +++++++++++++++++++ .../get_service_node_metadata.spec.ts | 91 +++++++++++++++++ 13 files changed, 244 insertions(+), 19 deletions(-) create mode 100644 x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts create mode 100644 x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts b/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts index a7a826d144d0e..e0a48fdcf2b89 100644 --- a/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts @@ -15,6 +15,9 @@ export type ApmApplicationMetricFields = Partial<{ 'system.cpu.total.norm.pct': number; 'system.process.memory.rss.bytes': number; 'system.process.cpu.total.norm.pct': number; + 'jvm.memory.heap.used': number; + 'jvm.memory.non_heap.used': number; + 'jvm.thread.count': number; }>; export type ApmUserAgentFields = Partial<{ diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/service.ts b/packages/elastic-apm-synthtrace/src/lib/apm/service.ts index 16917821c7ee4..d55f60d86e4db 100644 --- a/packages/elastic-apm-synthtrace/src/lib/apm/service.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/service.ts @@ -15,6 +15,7 @@ export class Service extends Entity { return new Instance({ ...this.fields, ['service.node.name']: instanceName, + 'host.name': instanceName, 'container.id': instanceName, }); } diff --git a/packages/elastic-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts b/packages/elastic-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts index b38d34266f3ac..a78f1ec987bcf 100644 --- a/packages/elastic-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts +++ b/packages/elastic-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts @@ -70,6 +70,7 @@ describe('simple trace', () => { 'agent.name': 'java', 'container.id': 'instance-1', 'event.outcome': 'success', + 'host.name': 'instance-1', 'processor.event': 'transaction', 'processor.name': 'transaction', 'service.environment': 'production', @@ -92,6 +93,7 @@ describe('simple trace', () => { 'agent.name': 'java', 'container.id': 'instance-1', 'event.outcome': 'success', + 'host.name': 'instance-1', 'parent.id': '0000000000000300', 'processor.event': 'span', 'processor.name': 'transaction', diff --git a/packages/elastic-apm-synthtrace/src/test/scenarios/__snapshots__/01_simple_trace.test.ts.snap b/packages/elastic-apm-synthtrace/src/test/scenarios/__snapshots__/01_simple_trace.test.ts.snap index 76a76d41ec81d..1a5fca39e9fd9 100644 --- a/packages/elastic-apm-synthtrace/src/test/scenarios/__snapshots__/01_simple_trace.test.ts.snap +++ b/packages/elastic-apm-synthtrace/src/test/scenarios/__snapshots__/01_simple_trace.test.ts.snap @@ -7,6 +7,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -24,6 +25,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000000", "processor.event": "span", "processor.name": "transaction", @@ -43,6 +45,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -60,6 +63,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000004", "processor.event": "span", "processor.name": "transaction", @@ -79,6 +83,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -96,6 +101,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000008", "processor.event": "span", "processor.name": "transaction", @@ -115,6 +121,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -132,6 +139,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000012", "processor.event": "span", "processor.name": "transaction", @@ -151,6 +159,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -168,6 +177,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000016", "processor.event": "span", "processor.name": "transaction", @@ -187,6 +197,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -204,6 +215,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000020", "processor.event": "span", "processor.name": "transaction", @@ -223,6 +235,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -240,6 +253,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000024", "processor.event": "span", "processor.name": "transaction", @@ -259,6 +273,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -276,6 +291,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000028", "processor.event": "span", "processor.name": "transaction", @@ -295,6 +311,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -312,6 +329,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000032", "processor.event": "span", "processor.name": "transaction", @@ -331,6 +349,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -348,6 +367,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000036", "processor.event": "span", "processor.name": "transaction", @@ -367,6 +387,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -384,6 +405,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000040", "processor.event": "span", "processor.name": "transaction", @@ -403,6 +425,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -420,6 +443,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000044", "processor.event": "span", "processor.name": "transaction", @@ -439,6 +463,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -456,6 +481,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000048", "processor.event": "span", "processor.name": "transaction", @@ -475,6 +501,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -492,6 +519,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000052", "processor.event": "span", "processor.name": "transaction", @@ -511,6 +539,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "processor.event": "transaction", "processor.name": "transaction", "service.environment": "production", @@ -528,6 +557,7 @@ Array [ "agent.name": "java", "container.id": "instance-1", "event.outcome": "success", + "host.name": "instance-1", "parent.id": "0000000000000056", "processor.event": "span", "processor.name": "transaction", diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 6da21bf2bf2c7..5dd3588674179 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -73,11 +73,11 @@ Object { } `; -exports[`Error HOST_NAME 1`] = `"my hostname"`; +exports[`Error HOST_HOSTNAME 1`] = `"my hostname"`; -exports[`Error HOST_OS_PLATFORM 1`] = `undefined`; +exports[`Error HOST_NAME 1`] = `undefined`; -exports[`Error HOSTNAME 1`] = `undefined`; +exports[`Error HOST_OS_PLATFORM 1`] = `undefined`; exports[`Error HTTP_REQUEST_METHOD 1`] = `undefined`; @@ -314,12 +314,12 @@ exports[`Span FID_FIELD 1`] = `undefined`; exports[`Span HOST 1`] = `undefined`; +exports[`Span HOST_HOSTNAME 1`] = `undefined`; + exports[`Span HOST_NAME 1`] = `undefined`; exports[`Span HOST_OS_PLATFORM 1`] = `undefined`; -exports[`Span HOSTNAME 1`] = `undefined`; - exports[`Span HTTP_REQUEST_METHOD 1`] = `undefined`; exports[`Span HTTP_RESPONSE_STATUS_CODE 1`] = `undefined`; @@ -555,11 +555,11 @@ Object { } `; -exports[`Transaction HOST_NAME 1`] = `"my hostname"`; +exports[`Transaction HOST_HOSTNAME 1`] = `"my hostname"`; -exports[`Transaction HOST_OS_PLATFORM 1`] = `undefined`; +exports[`Transaction HOST_NAME 1`] = `undefined`; -exports[`Transaction HOSTNAME 1`] = `undefined`; +exports[`Transaction HOST_OS_PLATFORM 1`] = `undefined`; exports[`Transaction HTTP_REQUEST_METHOD 1`] = `"GET"`; diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index b42c23ee2df94..5c7c953d8d900 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -113,8 +113,8 @@ export const METRICSET_NAME = 'metricset.name'; export const LABEL_NAME = 'labels.name'; export const HOST = 'host'; -export const HOST_NAME = 'host.hostname'; -export const HOSTNAME = 'host.name'; +export const HOST_HOSTNAME = 'host.hostname'; // Do not use. Please use `HOST_NAME` instead. +export const HOST_NAME = 'host.name'; export const HOST_OS_PLATFORM = 'host.os.platform'; export const CONTAINER_ID = 'container.id'; export const KUBERNETES = 'kubernetes'; diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx index 9b4d8e7d52a28..4f1c517d14b26 100644 --- a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx @@ -17,7 +17,7 @@ import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { CONTAINER_ID, - HOSTNAME, + HOST_NAME, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { useApmParams } from '../../../hooks/use_apm_params'; @@ -103,7 +103,7 @@ export const getInfrastructureKQLFilter = ( const infraAttributes = containerIds.length ? containerIds.map((id) => `${CONTAINER_ID}: "${id}"`) - : hostNames.map((id) => `${HOSTNAME}: "${id}"`); + : hostNames.map((id) => `${HOST_NAME}: "${id}"`); const infraAttributesJoined = infraAttributes.join(' or '); diff --git a/x-pack/plugins/apm/server/routes/service_nodes/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/routes/service_nodes/__snapshots__/queries.test.ts.snap index e0591a90b1c19..5022521c46914 100644 --- a/x-pack/plugins/apm/server/routes/service_nodes/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/routes/service_nodes/__snapshots__/queries.test.ts.snap @@ -17,7 +17,7 @@ Object { }, "host": Object { "terms": Object { - "field": "host.hostname", + "field": "host.name", "size": 1, }, }, @@ -74,7 +74,7 @@ Object { }, "host": Object { "terms": Object { - "field": "host.hostname", + "field": "host.name", "size": 1, }, }, @@ -145,7 +145,7 @@ Object { "top_metrics": Object { "metrics": Array [ Object { - "field": "host.hostname", + "field": "host.name", }, ], "sort": Object { diff --git a/x-pack/plugins/apm/server/routes/service_nodes/get_service_nodes.ts b/x-pack/plugins/apm/server/routes/service_nodes/get_service_nodes.ts index ebd56cb9768ce..58c105289be9c 100644 --- a/x-pack/plugins/apm/server/routes/service_nodes/get_service_nodes.ts +++ b/x-pack/plugins/apm/server/routes/service_nodes/get_service_nodes.ts @@ -109,7 +109,7 @@ const getServiceNodes = async ({ name: bucket.key as string, cpu: bucket.cpu.value, heapMemory: bucket.heapMemory.value, - hostName: bucket.latest.top?.[0]?.metrics?.['host.hostname'] as + hostName: bucket.latest.top?.[0]?.metrics?.[HOST_NAME] as | string | null | undefined, diff --git a/x-pack/plugins/apm/server/routes/services/get_service_infrastructure.ts b/x-pack/plugins/apm/server/routes/services/get_service_infrastructure.ts index cda0beb6b2d70..79d7ff4f1f41e 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_infrastructure.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_infrastructure.ts @@ -12,7 +12,7 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { SERVICE_NAME, CONTAINER_ID, - HOSTNAME, + HOST_NAME, } from '../../../common/elasticsearch_fieldnames'; export const getServiceInfrastructure = async ({ @@ -57,7 +57,7 @@ export const getServiceInfrastructure = async ({ }, hostNames: { terms: { - field: HOSTNAME, + field: HOST_NAME, size: 500, }, }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts index 1b20b82c1202c..35873a31150ac 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/elasticsearch_fieldnames.ts @@ -106,7 +106,7 @@ export const METRIC_JAVA_GC_TIME = 'jvm.gc.time'; export const LABEL_NAME = 'labels.name'; export const HOST = 'host'; -export const HOST_NAME = 'host.hostname'; +export const HOST_HOSTNAME = 'host.hostname'; export const HOST_OS_PLATFORM = 'host.os.platform'; export const CONTAINER_ID = 'container.id'; export const KUBERNETES = 'kubernetes'; diff --git a/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts b/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts new file mode 100644 index 0000000000000..8dac8f7dea4f9 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/service_nodes/get_service_nodes.spec.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { apm, timerange } from '@elastic/apm-synthtrace'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const registry = getService('registry'); + const synthtraceEsClient = getService('synthtraceEsClient'); + + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + const serviceName = 'synth-go'; + const instanceName = 'instance-a'; + + async function callApi() { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/serviceNodes', + params: { + path: { serviceName }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + kuery: '', + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + } + + registry.when('Service nodes when data is not loaded', { config: 'basic', archives: [] }, () => { + it('handles the empty state', async () => { + const response = await callApi(); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "serviceNodes": Array [], + } + `); + }); + }); + + registry.when( + 'Service nodes when data is loaded', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + before(async () => { + const instance = apm.service(serviceName, 'production', 'go').instance(instanceName); + await synthtraceEsClient.index( + timerange(start, end) + .interval('1m') + .rate(1) + .flatMap((timestamp) => + instance + .appMetrics({ + 'system.process.cpu.total.norm.pct': 1, + 'jvm.memory.heap.used': 1000, + 'jvm.memory.non_heap.used': 100, + 'jvm.thread.count': 25, + }) + .timestamp(timestamp) + .serialize() + ) + ); + }); + after(() => synthtraceEsClient.clean()); + + it('returns service nodes', async () => { + const response = await callApi(); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "serviceNodes": Array [ + Object { + "cpu": 1, + "heapMemory": 1000, + "hostName": "instance-a", + "name": "instance-a", + "nonHeapMemory": 100, + "threadCount": 25, + }, + ], + } + `); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts b/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts new file mode 100644 index 0000000000000..1317ce7e9a1ee --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/services/get_service_node_metadata.spec.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { apm, timerange } from '@elastic/apm-synthtrace'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const registry = getService('registry'); + const synthtraceEsClient = getService('synthtraceEsClient'); + + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + const serviceName = 'synth-go'; + const instanceName = 'instance-a'; + + async function callApi() { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/node/{serviceNodeName}/metadata', + params: { + path: { serviceName, serviceNodeName: instanceName }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + kuery: '', + }, + }, + }); + } + + registry.when( + 'Service node metadata when data is not loaded', + { config: 'basic', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await callApi(); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "containerId": "N/A", + "host": "N/A", + } + `); + }); + } + ); + + registry.when( + 'Service node metadata when data is loaded', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + before(async () => { + const instance = apm.service(serviceName, 'production', 'go').instance(instanceName); + await synthtraceEsClient.index( + timerange(start, end) + .interval('1m') + .rate(1) + .flatMap((timestamp) => + instance + .transaction('GET /api/product/list') + .timestamp(timestamp) + .duration(1000) + .success() + .serialize() + ) + ); + }); + after(() => synthtraceEsClient.clean()); + + it('returns service node metadata', async () => { + const response = await callApi(); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "containerId": "instance-a", + "host": "instance-a", + } + `); + }); + } + ); +} From c743c97c2d4f4b28465c7486d22d13431e178a59 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Mon, 13 Dec 2021 14:31:25 +0100 Subject: [PATCH 028/145] Fix wrong runtime field format on alert table (#120744) * Fix wrong runtime field format on alert table * Fix CI Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/types/timeline/index.ts | 2 +- .../common/types/timeline/columns/index.tsx | 4 +- .../body/column_headers/helpers.test.tsx | 102 ++++++++++++++++++ .../t_grid/body/column_headers/helpers.tsx | 9 +- 4 files changed, 107 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 60fd126e6fd85..442986870ac94 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -633,7 +633,7 @@ export interface ColumnHeaderResult { category?: Maybe; columnHeaderType?: Maybe; description?: Maybe; - example?: Maybe; + example?: Maybe; indexes?: Maybe; id?: Maybe; name?: Maybe; diff --git a/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx b/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx index 88caa779d0592..fd758e74df0e9 100644 --- a/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx +++ b/x-pack/plugins/timelines/common/types/timeline/columns/index.tsx @@ -76,8 +76,8 @@ export type ColumnHeaderOptions = Pick< tGridCellActions?: TGridCellAction[]; category?: string; columnHeaderType: ColumnHeaderType; - description?: string; - example?: string; + description?: string | null; + example?: string | number | null; format?: string; linkField?: string; placeholder?: string; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx index 9de28b870aadb..73287c3cf5cec 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx @@ -319,6 +319,108 @@ describe('helpers', () => { getColumnHeaders([headerDoesNotMatchBrowserField], mockBrowserFields).map(omit('display')) ).toEqual(expected); }); + + describe('augment the `header` with metadata from `browserFields`', () => { + test('it should augment the `header` when field category is base', () => { + const fieldName = 'test_field'; + const testField = { + aggregatable: true, + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: 'date', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: fieldName, + searchable: true, + type: 'date', + }; + + const browserField = { base: { fields: { [fieldName]: testField } } }; + + const header: ColumnHeaderOptions = { + columnHeaderType: 'not-filtered', + id: fieldName, + }; + + expect( + getColumnHeaders([header], browserField).map( + omit(['display', 'actions', 'isSortable', 'defaultSortDirection', 'schema']) + ) + ).toEqual([ + { + ...header, + ...browserField.base.fields[fieldName], + }, + ]); + }); + + test("it should augment the `header` when field is top level and name isn't splittable", () => { + const fieldName = 'testFieldName'; + const testField = { + aggregatable: true, + category: fieldName, + description: 'test field description', + example: '2016-05-23T08:05:34.853Z', + format: 'date', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: fieldName, + searchable: true, + type: 'date', + }; + + const browserField = { [fieldName]: { fields: { [fieldName]: testField } } }; + + const header: ColumnHeaderOptions = { + columnHeaderType: 'not-filtered', + id: fieldName, + }; + + expect( + getColumnHeaders([header], browserField).map( + omit(['display', 'actions', 'isSortable', 'defaultSortDirection', 'schema']) + ) + ).toEqual([ + { + ...header, + ...browserField[fieldName].fields[fieldName], + }, + ]); + }); + + test('it should augment the `header` when field is splittable', () => { + const fieldName = 'test.field.splittable'; + const testField = { + aggregatable: true, + category: 'test', + description: 'test field description', + example: '2016-05-23T08:05:34.853Z', + format: 'date', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: fieldName, + searchable: true, + type: 'date', + }; + + const browserField = { test: { fields: { [fieldName]: testField } } }; + + const header: ColumnHeaderOptions = { + columnHeaderType: 'not-filtered', + id: fieldName, + }; + + expect( + getColumnHeaders([header], browserField).map( + omit(['display', 'actions', 'isSortable', 'defaultSortDirection', 'schema']) + ) + ).toEqual([ + { + ...header, + ...browserField.test.fields[fieldName], + }, + ]); + }); + }); }); describe('getActionsColumnWidth', () => { diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx index aa2fc3f964c3c..1c2ac89119abb 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx @@ -7,7 +7,7 @@ import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; import { EuiDataGridColumnActions } from '@elastic/eui'; -import { get, keyBy } from 'lodash/fp'; +import { keyBy } from 'lodash/fp'; import React from 'react'; import type { @@ -91,17 +91,12 @@ export const getColumnHeaders = ( const browserFieldByName = getAllFieldsByName(browserFields); return headers ? headers.map((header) => { - const splitHeader = header.id.split('.'); // source.geo.city_name -> [source, geo, city_name] - const browserField: Partial | undefined = browserFieldByName[header.id]; // augment the header with metadata from browserFields: const augmentedHeader = { ...header, - ...get( - [splitHeader.length > 1 ? splitHeader[0] : 'base', 'fields', header.id], - browserFields - ), + ...browserField, schema: header.schema ?? getSchema(browserField?.type), }; From 30786a6e71428b5e92ab6b3a0351c21fe9ec0faa Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 13 Dec 2021 13:40:43 +0000 Subject: [PATCH 029/145] skip flaky suite (#104249) --- .../functional/apps/uptime/feature_controls/uptime_security.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts index c1ba546864a53..4d4acbe6242ba 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts @@ -136,7 +136,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('no uptime privileges', () => { + // FLAKY: https://github.com/elastic/kibana/issues/104249 + describe.skip('no uptime privileges', () => { before(async () => { await security.role.create('no_uptime_privileges_role', { elasticsearch: { From 862f1a9239701eb7c8ddcb0b75273f8c9287f5b2 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 13 Dec 2021 13:44:52 +0000 Subject: [PATCH 030/145] skip flaky suite (#120440) --- .../apps/observability/alerts/pagination.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/observability_functional/apps/observability/alerts/pagination.ts b/x-pack/test/observability_functional/apps/observability/alerts/pagination.ts index 79eadac3d7e40..9e80ea8f9e526 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/pagination.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/pagination.ts @@ -89,7 +89,8 @@ export default ({ getService }: FtrProviderContext) => { }); }); - describe('Pagination controls', () => { + // FLAKY: https://github.com/elastic/kibana/issues/120440 + describe.skip('Pagination controls', () => { before(async () => { await (await observability.alerts.pagination.getPageSizeSelector()).click(); await (await observability.alerts.pagination.getTenRowsPageSelector()).click(); From 38aa34f4964f80bc76006e0c852c3e5516305e11 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 13 Dec 2021 13:47:32 +0000 Subject: [PATCH 031/145] skip flaky suite (#119267) --- x-pack/test/security_api_integration/tests/audit/audit_log.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_api_integration/tests/audit/audit_log.ts b/x-pack/test/security_api_integration/tests/audit/audit_log.ts index bb4c27976c857..7322a2638767b 100644 --- a/x-pack/test/security_api_integration/tests/audit/audit_log.ts +++ b/x-pack/test/security_api_integration/tests/audit/audit_log.ts @@ -44,7 +44,8 @@ export default function ({ getService }: FtrProviderContext) { const retry = getService('retry'); const { username, password } = getService('config').get('servers.kibana'); - describe('Audit Log', function () { + // FLAKY: https://github.com/elastic/kibana/issues/119267 + describe.skip('Audit Log', function () { const logFilePath = Path.resolve(__dirname, '../../fixtures/audit/audit.log'); const logFile = new FileWrapper(logFilePath); From 6e62f5a6e12df77a3b829c714549698cce0f39a6 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 13 Dec 2021 13:51:20 +0000 Subject: [PATCH 032/145] skip flaky suite (#117780) --- .../functional/apps/maps/embeddable/tooltip_filter_actions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js index 09301fec40771..400c687d6af88 100644 --- a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js +++ b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js @@ -41,7 +41,8 @@ export default function ({ getPageObjects, getService }) { await security.testUser.restoreDefaults(); }); - describe('apply filter to current view', () => { + // FLAKY: https://github.com/elastic/kibana/issues/117780 + describe.skip('apply filter to current view', () => { before(async () => { await loadDashboardAndOpenTooltip(); }); From 205f77c8ff99954c7077f7701b6994657b40fd7b Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Mon, 13 Dec 2021 15:11:51 +0100 Subject: [PATCH 033/145] [Security Solution] Fixes upgrade tests (#120163) * avoids the threshold test execution when the version is not the correct * fixes version format * updates details to align with the new data * changes the assertion of the reason field depending on the version * improves the navigation to the rule * removes flakiness * adds missing commit Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../custom_query_rule.spec.ts | 30 ++++++++++----- .../detection_rules}/threshold_rule.spec.ts | 37 ++++++++++++------- .../cases}/import_case.spec.ts | 21 ++++++----- .../timeline}/import_timeline.spec.ts | 15 +++----- x-pack/plugins/security_solution/package.json | 1 + .../test/security_solution_cypress/runner.ts | 11 +++++- 6 files changed, 72 insertions(+), 43 deletions(-) rename x-pack/plugins/security_solution/cypress/upgrade_integration/{ => detections/detection_rules}/custom_query_rule.spec.ts (80%) rename x-pack/plugins/security_solution/cypress/upgrade_integration/{ => detections/detection_rules}/threshold_rule.spec.ts (80%) rename x-pack/plugins/security_solution/cypress/upgrade_integration/{ => threat_hunting/cases}/import_case.spec.ts (90%) rename x-pack/plugins/security_solution/cypress/upgrade_integration/{ => threat_hunting/timeline}/import_timeline.spec.ts (93%) diff --git a/x-pack/plugins/security_solution/cypress/upgrade_integration/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/upgrade_integration/detections/detection_rules/custom_query_rule.spec.ts similarity index 80% rename from x-pack/plugins/security_solution/cypress/upgrade_integration/custom_query_rule.spec.ts rename to x-pack/plugins/security_solution/cypress/upgrade_integration/detections/detection_rules/custom_query_rule.spec.ts index e4464ae43dd62..efc0d290ac728 100644 --- a/x-pack/plugins/security_solution/cypress/upgrade_integration/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/upgrade_integration/detections/detection_rules/custom_query_rule.spec.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import semver from 'semver'; import { DESTINATION_IP, HOST_NAME, @@ -14,8 +15,8 @@ import { SEVERITY, SOURCE_IP, USER_NAME, -} from '../screens/alerts'; -import { SERVER_SIDE_EVENT_COUNT } from '../screens/alerts_detection_rules'; +} from '../../../screens/alerts'; +import { SERVER_SIDE_EVENT_COUNT } from '../../../screens/alerts_detection_rules'; import { ADDITIONAL_LOOK_BACK_DETAILS, ABOUT_DETAILS, @@ -31,13 +32,16 @@ import { SCHEDULE_DETAILS, SEVERITY_DETAILS, TIMELINE_TEMPLATE_DETAILS, -} from '../screens/rule_details'; +} from '../../../screens/rule_details'; -import { waitForPageToBeLoaded } from '../tasks/common'; -import { waitForRulesTableToBeLoaded, goToTheRuleDetailsOf } from '../tasks/alerts_detection_rules'; -import { loginAndWaitForPage } from '../tasks/login'; +import { waitForPageToBeLoaded } from '../../../tasks/common'; +import { + waitForRulesTableToBeLoaded, + goToTheRuleDetailsOf, +} from '../../../tasks/alerts_detection_rules'; +import { loginAndWaitForPage } from '../../../tasks/login'; -import { DETECTIONS_RULE_MANAGEMENT_URL } from '../urls/navigation'; +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; const EXPECTED_NUMBER_OF_ALERTS = '1'; @@ -63,8 +67,8 @@ const rule = { severity: 'Low', riskScore: '7', timelineTemplate: 'none', - runsEvery: '10s', - lookBack: '179999990s', + runsEvery: '24h', + lookBack: '49976h', timeline: 'None', }; @@ -100,10 +104,16 @@ describe('After an upgrade, the custom query rule', () => { }); it('Displays the alert details at the tgrid', () => { + let expectedReason; + if (semver.gt(Cypress.env('ORIGINAL_VERSION'), '7.15.0')) { + expectedReason = alert.reason; + } else { + expectedReason = '-'; + } cy.get(RULE_NAME).should('have.text', alert.rule); cy.get(SEVERITY).should('have.text', alert.severity); cy.get(RISK_SCORE).should('have.text', alert.riskScore); - cy.get(REASON).should('have.text', alert.reason).type('{rightarrow}'); + cy.get(REASON).should('have.text', expectedReason).type('{rightarrow}'); cy.get(HOST_NAME).should('have.text', alert.hostName); cy.get(USER_NAME).should('have.text', alert.username); cy.get(PROCESS_NAME).should('have.text', alert.processName); diff --git a/x-pack/plugins/security_solution/cypress/upgrade_integration/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/upgrade_integration/detections/detection_rules/threshold_rule.spec.ts similarity index 80% rename from x-pack/plugins/security_solution/cypress/upgrade_integration/threshold_rule.spec.ts rename to x-pack/plugins/security_solution/cypress/upgrade_integration/detections/detection_rules/threshold_rule.spec.ts index b6dbcd0e3232c..16949c9b34c63 100644 --- a/x-pack/plugins/security_solution/cypress/upgrade_integration/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/upgrade_integration/detections/detection_rules/threshold_rule.spec.ts @@ -4,9 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { HOST_NAME, REASON, RISK_SCORE, RULE_NAME, SEVERITY } from '../screens/alerts'; -import { SERVER_SIDE_EVENT_COUNT } from '../screens/alerts_detection_rules'; +import semver from 'semver'; +import { HOST_NAME, REASON, RISK_SCORE, RULE_NAME, SEVERITY } from '../../../screens/alerts'; +import { SERVER_SIDE_EVENT_COUNT } from '../../../screens/alerts_detection_rules'; import { ADDITIONAL_LOOK_BACK_DETAILS, ABOUT_DETAILS, @@ -23,14 +23,17 @@ import { SEVERITY_DETAILS, THRESHOLD_DETAILS, TIMELINE_TEMPLATE_DETAILS, -} from '../screens/rule_details'; +} from '../../../screens/rule_details'; -import { expandFirstAlert } from '../tasks/alerts'; -import { waitForPageToBeLoaded } from '../tasks/common'; -import { waitForRulesTableToBeLoaded, goToRuleDetails } from '../tasks/alerts_detection_rules'; -import { loginAndWaitForPage } from '../tasks/login'; +import { expandFirstAlert } from '../../../tasks/alerts'; +import { waitForPageToBeLoaded } from '../../../tasks/common'; +import { + goToTheRuleDetailsOf, + waitForRulesTableToBeLoaded, +} from '../../../tasks/alerts_detection_rules'; +import { loginAndWaitForPage } from '../../../tasks/login'; -import { DETECTIONS_RULE_MANAGEMENT_URL } from '../urls/navigation'; +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; import { OVERVIEW_HOST_NAME, OVERVIEW_RISK_SCORE, @@ -40,7 +43,7 @@ import { OVERVIEW_THRESHOLD_COUNT, OVERVIEW_THRESHOLD_VALUE, SUMMARY_VIEW, -} from '../screens/alerts_details'; +} from '../../../screens/alerts_details'; const EXPECTED_NUMBER_OF_ALERTS = '1'; @@ -61,8 +64,8 @@ const rule = { severity: 'Medium', riskScore: '17', timelineTemplate: 'none', - runsEvery: '60s', - lookBack: '2999999m', + runsEvery: '24h', + lookBack: '49976h', timeline: 'None', thresholdField: 'host.name', threholdValue: '1', @@ -72,7 +75,7 @@ describe('After an upgrade, the threshold rule', () => { before(() => { loginAndWaitForPage(DETECTIONS_RULE_MANAGEMENT_URL); waitForRulesTableToBeLoaded(); - goToRuleDetails(); + goToTheRuleDetailsOf(rule.name); waitForPageToBeLoaded(); }); @@ -104,10 +107,16 @@ describe('After an upgrade, the threshold rule', () => { }); it('Displays the alert details in the TGrid', () => { + let expectedReason; + if (semver.gt(Cypress.env('ORIGINAL_VERSION'), '7.15.0')) { + expectedReason = alert.reason; + } else { + expectedReason = '-'; + } cy.get(RULE_NAME).should('have.text', alert.rule); cy.get(SEVERITY).should('have.text', alert.severity); cy.get(RISK_SCORE).should('have.text', alert.riskScore); - cy.get(REASON).should('have.text', alert.reason); + cy.get(REASON).should('have.text', expectedReason); cy.get(HOST_NAME).should('have.text', alert.hostName); }); diff --git a/x-pack/plugins/security_solution/cypress/upgrade_integration/import_case.spec.ts b/x-pack/plugins/security_solution/cypress/upgrade_integration/threat_hunting/cases/import_case.spec.ts similarity index 90% rename from x-pack/plugins/security_solution/cypress/upgrade_integration/import_case.spec.ts rename to x-pack/plugins/security_solution/cypress/upgrade_integration/threat_hunting/cases/import_case.spec.ts index eb72dea9be7e8..e97cebeff00b5 100644 --- a/x-pack/plugins/security_solution/cypress/upgrade_integration/import_case.spec.ts +++ b/x-pack/plugins/security_solution/cypress/upgrade_integration/threat_hunting/cases/import_case.spec.ts @@ -15,7 +15,7 @@ import { ALL_CASES_OPEN_CASES_STATS, ALL_CASES_REPORTER, ALL_CASES_IN_PROGRESS_STATUS, -} from '../screens/all_cases'; +} from '../../../screens/all_cases'; import { CASES_TAGS, CASE_CONNECTOR, @@ -25,16 +25,19 @@ import { CASE_IN_PROGRESS_STATUS, CASE_SWITCH, CASE_USER_ACTION, -} from '../screens/case_details'; -import { CASES_PAGE } from '../screens/kibana_navigation'; +} from '../../../screens/case_details'; +import { CASES_PAGE } from '../../../screens/kibana_navigation'; -import { goToCaseDetails } from '../tasks/all_cases'; -import { deleteCase } from '../tasks/case_details'; -import { navigateFromKibanaCollapsibleTo, openKibanaNavigation } from '../tasks/kibana_navigation'; -import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; -import { importCase } from '../tasks/saved_objects'; +import { goToCaseDetails } from '../../../tasks/all_cases'; +import { deleteCase } from '../../../tasks/case_details'; +import { + navigateFromKibanaCollapsibleTo, + openKibanaNavigation, +} from '../../../tasks/kibana_navigation'; +import { loginAndWaitForPageWithoutDateRange } from '../../../tasks/login'; +import { importCase } from '../../../tasks/saved_objects'; -import { KIBANA_SAVED_OBJECTS } from '../urls/navigation'; +import { KIBANA_SAVED_OBJECTS } from '../../../urls/navigation'; const CASE_NDJSON = '7_16_case.ndjson'; const importedCase = { diff --git a/x-pack/plugins/security_solution/cypress/upgrade_integration/import_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/upgrade_integration/threat_hunting/timeline/import_timeline.spec.ts similarity index 93% rename from x-pack/plugins/security_solution/cypress/upgrade_integration/import_timeline.spec.ts rename to x-pack/plugins/security_solution/cypress/upgrade_integration/threat_hunting/timeline/import_timeline.spec.ts index f3b3f14e9c260..253a1c9c59b0b 100644 --- a/x-pack/plugins/security_solution/cypress/upgrade_integration/import_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/upgrade_integration/threat_hunting/timeline/import_timeline.spec.ts @@ -28,7 +28,7 @@ import { TIMELINE_QUERY, TIMELINE_TITLE, USER_KPI, -} from '../screens/timeline'; +} from '../../../screens/timeline'; import { NOTE, TIMELINES_USERNAME, @@ -36,19 +36,19 @@ import { TIMELINES_DESCRIPTION, TIMELINES_NOTES_COUNT, TIMELINES_PINNED_EVENT_COUNT, -} from '../screens/timelines'; +} from '../../../screens/timelines'; -import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { loginAndWaitForPageWithoutDateRange } from '../../../tasks/login'; import { closeTimeline, deleteTimeline, goToCorrelationTab, goToNotesTab, goToPinnedTab, -} from '../tasks/timeline'; -import { expandNotes, importTimeline, openTimeline } from '../tasks/timelines'; +} from '../../../tasks/timeline'; +import { expandNotes, importTimeline, openTimeline } from '../../../tasks/timelines'; -import { TIMELINES_URL } from '../urls/navigation'; +import { TIMELINES_URL } from '../../../urls/navigation'; const timeline = '7_15_timeline.ndjson'; const username = 'elastic'; @@ -64,7 +64,6 @@ const timelineDetails = { }; const detectionAlert = { - timestamp: 'Nov 17, 2021 @ 09:36:25.499', message: '—', eventCategory: 'file', eventAction: 'initial_scan', @@ -149,7 +148,6 @@ describe('Import timeline after upgrade', () => { cy.get(NOTES_TAB_BUTTON).should('have.text', timelineDetails.notesTab); cy.get(PINNED_TAB_BUTTON).should('have.text', timelineDetails.pinnedTab); - cy.get(QUERY_EVENT_TABLE_CELL).eq(0).should('contain', detectionAlert.timestamp); cy.get(QUERY_EVENT_TABLE_CELL).eq(1).should('contain', detectionAlert.message); cy.get(QUERY_EVENT_TABLE_CELL).eq(2).should('contain', detectionAlert.eventCategory); cy.get(QUERY_EVENT_TABLE_CELL).eq(3).should('contain', detectionAlert.eventAction); @@ -196,7 +194,6 @@ describe('Import timeline after upgrade', () => { it('Displays the correct timeline details inside the pinned tab', () => { goToPinnedTab(); - cy.get(PINNED_EVENT_TABLE_CELL).eq(0).should('contain', detectionAlert.timestamp); cy.get(PINNED_EVENT_TABLE_CELL).eq(1).should('contain', detectionAlert.message); cy.get(PINNED_EVENT_TABLE_CELL).eq(2).should('contain', detectionAlert.eventCategory); cy.get(PINNED_EVENT_TABLE_CELL).eq(3).should('contain', detectionAlert.eventAction); diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 371ac66004f48..821550f21919a 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -19,6 +19,7 @@ "cypress:run-as-ci": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/cli_config.ts", "cypress:run-as-ci:firefox": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/config.firefox.ts", "cypress:run:upgrade": "yarn cypress:run:reporter --browser chrome --config integrationFolder=./cypress/upgrade_integration", + "cypress:run:upgrade:old": "yarn cypress:run:reporter --browser chrome --config integrationFolder=./cypress/upgrade_integration --spec ./cypress/upgrade_integration/threat_hunting/**/*.spec.ts,./cypress/upgrade_integration/detections/**/custom_query_rule.spec.ts; status=$?; yarn junit:merge && exit $status", "junit:merge": "../../../node_modules/.bin/mochawesome-merge ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../target/kibana-security-solution/cypress/results/output.json && ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results && mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/", "test:generate": "node scripts/endpoint/resolver_generator" } diff --git a/x-pack/test/security_solution_cypress/runner.ts b/x-pack/test/security_solution_cypress/runner.ts index a111c327b1ac6..b537be4e80b41 100644 --- a/x-pack/test/security_solution_cypress/runner.ts +++ b/x-pack/test/security_solution_cypress/runner.ts @@ -10,6 +10,7 @@ import Url from 'url'; import { withProcRunner } from '@kbn/dev-utils'; +import semver from 'semver'; import { FtrProviderContext } from './ftr_provider_context'; export async function SecuritySolutionCypressCliTestRunner({ getService }: FtrProviderContext) { @@ -117,11 +118,18 @@ export async function SecuritySolutionCypressUpgradeCliTestRunner({ getService, }: FtrProviderContext) { const log = getService('log'); + let command = ''; + + if (semver.gt(process.env.ORIGINAL_VERSION!, '7.10.0')) { + command = 'cypress:run:upgrade'; + } else { + command = 'cypress:run:upgrade:old'; + } await withProcRunner(log, async (procs) => { await procs.run('cypress', { cmd: 'yarn', - args: ['cypress:run:upgrade'], + args: [command], cwd: resolve(__dirname, '../../plugins/security_solution'), env: { FORCE_COLOR: '1', @@ -129,6 +137,7 @@ export async function SecuritySolutionCypressUpgradeCliTestRunner({ CYPRESS_ELASTICSEARCH_URL: process.env.TEST_ES_URL, CYPRESS_ELASTICSEARCH_USERNAME: process.env.TEST_ES_USER, CYPRESS_ELASTICSEARCH_PASSWORD: process.env.TEST_ES_PASS, + CYPRESS_ORIGINAL_VERSION: process.env.ORIGINAL_VERSION, ...process.env, }, wait: true, From b0442e396b360f788524cfe38de62fa31fc1d789 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 13 Dec 2021 16:21:48 +0200 Subject: [PATCH 034/145] [Lens] Applies new time axis for area and line charts with breakdown dimension (#120891) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../xy_visualization/expression.test.tsx | 111 ++++++++++++++++++ .../public/xy_visualization/expression.tsx | 3 +- 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index 65425b04129d3..027165a2eb5d0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -636,6 +636,117 @@ describe('xy_expression', () => { `); }); + describe('axis time', () => { + const defaultTimeLayer: LayerArgs = { + layerId: 'first', + layerType: layerTypes.DATA, + seriesType: 'line', + xAccessor: 'c', + accessors: ['a', 'b'], + splitAccessor: 'd', + columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', + xScaleType: 'time', + yScaleType: 'linear', + isHistogram: true, + palette: mockPaletteOutput, + }; + test('it should disable the new time axis for a line time layer when isHistogram is set to false', () => { + const { data } = sampleArgs(); + + const instance = shallow( + + ); + + const axisStyle = instance.find(Axis).first().prop('timeAxisLayerCount'); + + expect(axisStyle).toBe(0); + }); + test('it should enable the new time axis for a line time layer when isHistogram is set to true', () => { + const { data } = sampleArgs(); + const timeLayerArgs = createArgsWithLayers([defaultTimeLayer]); + + const instance = shallow( + + ); + + const axisStyle = instance.find(Axis).first().prop('timeAxisLayerCount'); + + expect(axisStyle).toBe(3); + }); + test('it should disable the new time axis for a vertical bar with break down dimension', () => { + const { data } = sampleArgs(); + const timeLayer: LayerArgs = { + ...defaultTimeLayer, + seriesType: 'bar', + }; + const timeLayerArgs = createArgsWithLayers([timeLayer]); + + const instance = shallow( + + ); + + const axisStyle = instance.find(Axis).first().prop('timeAxisLayerCount'); + + expect(axisStyle).toBe(0); + }); + + test('it should enable the new time axis for a stacked vertical bar with break down dimension', () => { + const { data } = sampleArgs(); + const timeLayer: LayerArgs = { + ...defaultTimeLayer, + seriesType: 'bar_stacked', + }; + const timeLayerArgs = createArgsWithLayers([timeLayer]); + + const instance = shallow( + + ); + + const axisStyle = instance.find(Axis).first().prop('timeAxisLayerCount'); + + expect(axisStyle).toBe(3); + }); + }); describe('endzones', () => { const { args } = sampleArgs(); const data: LensMultiTable = { diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 38709ae21d8fd..9c4c56281dae0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -562,9 +562,8 @@ export function XYChart({ } as LegendPositionConfig; const isHistogramModeEnabled = filteredLayers.some( - ({ isHistogram, seriesType, splitAccessor }) => + ({ isHistogram, seriesType }) => isHistogram && - (seriesType.includes('stacked') || !splitAccessor) && (seriesType.includes('stacked') || !seriesType.includes('bar') || !chartHasMoreThanOneBarSeries) From b6753241ed424d236c0ddfcc4f94437c188a7f12 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Mon, 13 Dec 2021 16:46:42 +0100 Subject: [PATCH 035/145] [Security Solution] host isolation exceptions listing under policy integration details tab (#120361) --- .../found_exception_list_item_schema.mock.ts | 8 +- .../public/management/common/constants.ts | 1 + .../public/management/common/routing.ts | 31 +++- .../public/management/common/utils.ts | 21 +++ .../host_isolation_exceptions/view/hooks.ts | 38 ++-- .../host_isolation_exceptions_list.test.tsx | 8 +- .../view/host_isolation_exceptions_list.tsx | 7 +- .../public/management/pages/policy/index.tsx | 2 + .../selectors/policy_common_selectors.ts | 18 +- .../selectors/policy_settings_selectors.ts | 8 +- .../components/empty_unassigned.tsx | 53 ++++++ .../components/empty_unexisting.tsx | 50 ++++++ .../components/list.test.tsx | 94 ++++++++++ .../components/list.tsx | 143 +++++++++++++++ .../host_isolation_exceptions_tab.test.tsx | 118 +++++++++++++ .../host_isolation_exceptions_tab.tsx | 164 ++++++++++++++++++ .../pages/policy/view/policy_details.test.tsx | 9 + .../pages/policy/view/tabs/policy_tabs.tsx | 69 +++++--- .../management/services/policies/hooks.ts | 4 +- 19 files changed, 793 insertions(+), 53 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unassigned.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unexisting.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.tsx diff --git a/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.mock.ts index d06ab90e84168..18143d765cae9 100644 --- a/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/found_exception_list_item_schema.mock.ts @@ -9,9 +9,11 @@ import type { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-l import { getExceptionListItemSchemaMock } from './exception_list_item_schema.mock'; -export const getFoundExceptionListItemSchemaMock = (): FoundExceptionListItemSchema => ({ - data: [getExceptionListItemSchemaMock()], +export const getFoundExceptionListItemSchemaMock = ( + count: number = 1 +): FoundExceptionListItemSchema => ({ + data: Array.from({ length: count }, getExceptionListItemSchemaMock), page: 1, per_page: 1, - total: 1, + total: count, }); diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts index e35c07edb65a4..d6603896c9cf8 100644 --- a/x-pack/plugins/security_solution/public/management/common/constants.ts +++ b/x-pack/plugins/security_solution/public/management/common/constants.ts @@ -14,6 +14,7 @@ export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_PATH}/:tabName(${A export const MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/settings`; export const MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/trustedApps`; export const MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/eventFilters`; +export const MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/hostIsolationExceptions`; /** @deprecated use the paths defined above instead */ export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH_OLD = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId`; export const MANAGEMENT_ROUTING_TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.trustedApps})`; diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index 9b4792567f9a8..44a389c5a3a46 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -6,10 +6,16 @@ */ import { isEmpty } from 'lodash/fp'; -import { generatePath } from 'react-router-dom'; // eslint-disable-next-line import/no-nodejs-modules import querystring from 'querystring'; - +import { generatePath } from 'react-router-dom'; +import { appendSearch } from '../../common/components/link_to/helpers'; +import { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types'; +import { EventFiltersPageLocation } from '../pages/event_filters/types'; +import { HostIsolationExceptionsPageLocation } from '../pages/host_isolation_exceptions/types'; +import { PolicyDetailsArtifactsPageLocation } from '../pages/policy/types'; +import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state'; +import { AdministrationSubTab } from '../types'; import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE, @@ -19,17 +25,11 @@ import { MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH, MANAGEMENT_ROUTING_POLICIES_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH, MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, } from './constants'; -import { AdministrationSubTab } from '../types'; -import { appendSearch } from '../../common/components/link_to/helpers'; -import { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types'; -import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state'; -import { EventFiltersPageLocation } from '../pages/event_filters/types'; -import { HostIsolationExceptionsPageLocation } from '../pages/host_isolation_exceptions/types'; -import { PolicyDetailsArtifactsPageLocation } from '../pages/policy/types'; // Taken from: https://github.com/microsoft/TypeScript/issues/12936#issuecomment-559034150 type ExactKeys = Exclude extends never ? T1 : never; @@ -390,3 +390,16 @@ export const getHostIsolationExceptionsListPath = ( querystring.stringify(normalizeHostIsolationExceptionsPageLocation(location)) )}`; }; + +export const getPolicyHostIsolationExceptionsPath = ( + policyId: string, + location?: Partial +) => { + const path = generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH, { + tabName: AdministrationSubTab.policies, + policyId, + }); + return `${path}${appendSearch( + querystring.stringify(normalizePolicyDetailsArtifactsListPageLocation(location)) + )}`; +}; diff --git a/x-pack/plugins/security_solution/public/management/common/utils.ts b/x-pack/plugins/security_solution/public/management/common/utils.ts index 3fbe5662f338c..e9f93e85bdb60 100644 --- a/x-pack/plugins/security_solution/public/management/common/utils.ts +++ b/x-pack/plugins/security_solution/public/management/common/utils.ts @@ -49,3 +49,24 @@ export const parsePoliciesToKQL = (includedPolicies: string, excludedPolicies: s return `(${kuery.join(' AND ')})`; }; + +/** + * Takes a list of policies (string[]) and an existing kuery + * (string) and returns an unified KQL with and AND + * @param policies string[] a list of policies ids + * @param kuery string an existing KQL. + */ +export const parsePoliciesAndFilterToKql = ({ + policies, + kuery, +}: { + policies?: string[]; + kuery?: string; +}): string | undefined => { + if (!policies || !policies.length) { + return kuery; + } + + const policiesKQL = parsePoliciesToKQL(policies.join(','), ''); + return `(${policiesKQL})${kuery ? ` AND (${kuery})` : ''}`; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts index 3cac7e7e466d6..f814cc9726deb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts @@ -22,7 +22,7 @@ import { MANAGEMENT_STORE_HOST_ISOLATION_EXCEPTIONS_NAMESPACE, } from '../../../common/constants'; import { getHostIsolationExceptionsListPath } from '../../../common/routing'; -import { parseQueryFilterToKQL } from '../../../common/utils'; +import { parsePoliciesAndFilterToKql, parseQueryFilterToKQL } from '../../../common/utils'; import { getHostIsolationExceptionItems, getHostIsolationExceptionSummary, @@ -87,23 +87,37 @@ export function useCanSeeHostIsolationExceptionsMenu(): boolean { const SEARCHABLE_FIELDS: Readonly = [`name`, `description`, `entries.value`]; -export function useFetchHostIsolationExceptionsList(): QueryObserverResult< - FoundExceptionListItemSchema, - ServerApiError -> { +export function useFetchHostIsolationExceptionsList({ + filter, + page, + perPage, + policies, + enabled = true, +}: { + filter?: string; + page: number; + perPage: number; + policies?: string[]; + enabled?: boolean; +}): QueryObserverResult { const http = useHttp(); - const location = useHostIsolationExceptionsSelector(getCurrentLocation); return useQuery( - ['hostIsolationExceptions', 'list', location.filter, location.page_size, location.page_index], + ['hostIsolationExceptions', 'list', filter, perPage, page, policies], () => { + const kql = parsePoliciesAndFilterToKql({ + policies, + kuery: filter ? parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) : undefined, + }); + return getHostIsolationExceptionItems({ http, - page: location.page_index + 1, - perPage: location.page_size, - filter: parseQueryFilterToKQL(location.filter, SEARCHABLE_FIELDS) || undefined, + page: page + 1, + perPage, + filter: kql, }); - } + }, + { enabled } ); } @@ -114,7 +128,7 @@ export function useGetHostIsolationExceptionFormEntry({ }: { id?: string; onSuccess: (data: CreateExceptionListItemSchema | UpdateExceptionListItemSchema) => void; - onError: (error: ServerApiError) => void; + onError?: (error: ServerApiError) => void; }): QueryObserverResult { const http = useHttp(); return useQuery( diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx index 94bd6ea73d7fa..c53371167c536 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx @@ -91,7 +91,9 @@ describe('When on the host isolation exceptions page', () => { describe('And data exists', () => { beforeEach(async () => { - getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock); + getHostIsolationExceptionItemsMock.mockImplementation(() => + getFoundExceptionListItemSchemaMock(1) + ); }); it('should show loading indicator while retrieving data and hide it when it gets it', async () => { @@ -185,7 +187,9 @@ describe('When on the host isolation exceptions page', () => { describe('has canIsolateHost privileges', () => { beforeEach(async () => { setEndpointPrivileges({ canIsolateHost: true }); - getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock); + getHostIsolationExceptionItemsMock.mockImplementation(() => + getFoundExceptionListItemSchemaMock(1) + ); }); it('should show the create flyout when the add button is pressed', async () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index d4dcb105cdb51..d883f47b34ff5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -55,7 +55,12 @@ export const HostIsolationExceptionsList = () => { const [itemToDelete, setItemToDelete] = useState(null); - const { isLoading, data, error, refetch } = useFetchHostIsolationExceptionsList(); + const { isLoading, data, error, refetch } = useFetchHostIsolationExceptionsList({ + filter: location.filter, + page: location.page_index, + perPage: location.page_size, + }); + const toasts = useToasts(); // load the list of policies> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx index e2c817b47a1b6..249345a0a0ad8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx @@ -13,6 +13,7 @@ import { MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_PATH_OLD, + MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH, } from '../../common/constants'; import { NotFoundPage } from '../../../app/404'; import { getPolicyDetailPath } from '../../common/routing'; @@ -25,6 +26,7 @@ export const PolicyContainer = memo(() => { MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH, ]} exact component={PolicyDetails} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_common_selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_common_selectors.ts index 5ac0ebca82ebb..40953b927e935 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_common_selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_common_selectors.ts @@ -7,12 +7,13 @@ import { matchPath } from 'react-router-dom'; import { createSelector } from 'reselect'; -import { PolicyDetailsSelector, PolicyDetailsState } from '../../../types'; import { MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH, } from '../../../../../common/constants'; +import { PolicyDetailsSelector, PolicyDetailsState } from '../../../types'; /** * Returns current artifacts location @@ -37,7 +38,7 @@ export const isOnPolicyFormView: PolicyDetailsSelector = createSelector } ); -/** Returns a boolean of whether the user is on the policy trusted app page or not */ +/** Returns a boolean of whether the user is on the policy trusted apps page or not */ export const isOnPolicyTrustedAppsView: PolicyDetailsSelector = createSelector( getUrlLocationPathname, (pathname) => { @@ -62,3 +63,16 @@ export const isOnPolicyEventFiltersView: PolicyDetailsSelector = create ); } ); + +/** Returns a boolean of whether the user is on the host isolation exceptions page or not */ +export const isOnHostIsolationExceptionsView: PolicyDetailsSelector = createSelector( + getUrlLocationPathname, + (pathname) => { + return ( + matchPath(pathname ?? '', { + path: MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH, + exact: true, + }) !== null + ); + } +); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts index 05b8edae48efd..a1a4c62d70734 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { createSelector } from 'reselect'; import { matchPath } from 'react-router-dom'; +import { createSelector } from 'reselect'; import { ILicense } from '../../../../../../../../licensing/common/types'; import { unsetPolicyFeaturesAccordingToLicenseLevel } from '../../../../../../../common/license/policy_config'; import { PolicyDetailsState } from '../../../types'; @@ -20,6 +20,7 @@ import { import { policyFactory as policyConfigFactory } from '../../../../../../../common/endpoint/models/policy_config'; import { MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH, } from '../../../../../common/constants'; @@ -28,6 +29,7 @@ import { getPolicyDataForUpdate } from '../../../../../../../common/endpoint/ser import { isOnPolicyTrustedAppsView, isOnPolicyEventFiltersView, + isOnHostIsolationExceptionsView, isOnPolicyFormView, } from './policy_common_selectors'; @@ -90,7 +92,8 @@ export const needsToRefresh = (state: Immutable): boolean => export const isOnPolicyDetailsPage = (state: Immutable) => isOnPolicyFormView(state) || isOnPolicyTrustedAppsView(state) || - isOnPolicyEventFiltersView(state); + isOnPolicyEventFiltersView(state) || + isOnHostIsolationExceptionsView(state); /** Returns the license info fetched from the license service */ export const license = (state: Immutable) => { @@ -107,6 +110,7 @@ export const policyIdFromParams: (state: Immutable) => strin MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_EVENT_FILTERS_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_HOST_ISOLATION_EXCEPTIONS_PATH, ], exact: true, })?.params?.policyId ?? '' diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unassigned.tsx new file mode 100644 index 0000000000000..e9f7fefe312da --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unassigned.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiEmptyPrompt, EuiLink, EuiPageTemplate } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; + +export const PolicyHostIsolationExceptionsEmptyUnassigned = ({ + policyName, + toHostIsolationList, +}: { + policyName: string; + toHostIsolationList: string; +}) => { + return ( + + + + + } + body={ + + } + actions={[ + + + , + ]} + /> + + ); +}; + +PolicyHostIsolationExceptionsEmptyUnassigned.displayName = + 'PolicyHostIsolationExceptionsEmptyUnassigned'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unexisting.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unexisting.tsx new file mode 100644 index 0000000000000..da3a48ac2570a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unexisting.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButton, EuiEmptyPrompt, EuiPageTemplate } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; + +export const PolicyHostIsolationExceptionsEmptyUnexisting = ({ + toHostIsolationList, +}: { + toHostIsolationList: string; +}) => { + return ( + + + + + } + body={ + + } + actions={ + + + + } + /> + + ); +}; + +PolicyHostIsolationExceptionsEmptyUnexisting.displayName = + 'PolicyHostIsolationExceptionsEmptyUnexisting'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.test.tsx new file mode 100644 index 0000000000000..cb126afac24e8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.test.tsx @@ -0,0 +1,94 @@ +/* + * 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 { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { act } from '@testing-library/react'; +import React from 'react'; +import uuid from 'uuid'; +import { getPolicyHostIsolationExceptionsPath } from '../../../../../common/routing'; +import { getFoundExceptionListItemSchemaMock } from '../../../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../../common/mock/endpoint'; +import { PolicyHostIsolationExceptionsList } from './list'; +import userEvent from '@testing-library/user-event'; + +const emptyList = { + data: [], + page: 1, + per_page: 10, + total: 0, +}; + +describe('Policy details host isolation exceptions tab', () => { + let policyId: string; + let render: ( + exceptions: FoundExceptionListItemSchema + ) => ReturnType; + let renderResult: ReturnType; + let history: AppContextTestRender['history']; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + policyId = uuid.v4(); + mockedContext = createAppRootMockRenderer(); + ({ history } = mockedContext); + render = (exceptions: FoundExceptionListItemSchema) => + (renderResult = mockedContext.render( + + )); + + act(() => { + history.push(getPolicyHostIsolationExceptionsPath(policyId)); + }); + }); + + it('should display a searchbar and count even with no exceptions', () => { + render(emptyList); + expect( + renderResult.getByTestId('policyDetailsHostIsolationExceptionsSearchCount') + ).toHaveTextContent('Showing 0 exceptions'); + expect(renderResult.getByTestId('searchField')).toBeTruthy(); + }); + + it('should render the list of exceptions collapsed and expand it when clicked', () => { + // render 3 + render(getFoundExceptionListItemSchemaMock(3)); + expect(renderResult.getAllByTestId('hostIsolationExceptions-collapsed-list-card')).toHaveLength( + 3 + ); + expect( + renderResult.queryAllByTestId( + 'hostIsolationExceptions-collapsed-list-card-criteriaConditions' + ) + ).toHaveLength(0); + }); + + it('should expand an item when expand is clicked', () => { + render(getFoundExceptionListItemSchemaMock(1)); + expect(renderResult.getAllByTestId('hostIsolationExceptions-collapsed-list-card')).toHaveLength( + 1 + ); + + userEvent.click( + renderResult.getByTestId('hostIsolationExceptions-collapsed-list-card-header-expandCollapse') + ); + + expect( + renderResult.queryAllByTestId( + 'hostIsolationExceptions-collapsed-list-card-criteriaConditions' + ) + ).toHaveLength(1); + }); + + it('should change the address location when a filter is applied', () => { + render(getFoundExceptionListItemSchemaMock(1)); + userEvent.type(renderResult.getByTestId('searchField'), 'search me{enter}'); + expect(history.location.search).toBe('?filter=search%20me'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx new file mode 100644 index 0000000000000..4bc52d7f191c1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx @@ -0,0 +1,143 @@ +/* + * 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 { EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import React, { useCallback, useMemo, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { + MANAGEMENT_DEFAULT_PAGE_SIZE, + MANAGEMENT_PAGE_SIZE_OPTIONS, +} from '../../../../../common/constants'; +import { getPolicyHostIsolationExceptionsPath } from '../../../../../common/routing'; +import { + ArtifactCardGrid, + ArtifactCardGridProps, +} from '../../../../../components/artifact_card_grid'; +import { useEndpointPoliciesToArtifactPolicies } from '../../../../../components/artifact_entry_card/hooks/use_endpoint_policies_to_artifact_policies'; +import { SearchExceptions } from '../../../../../components/search_exceptions'; +import { useGetEndpointSpecificPolicies } from '../../../../../services/policies/hooks'; +import { getCurrentArtifactsLocation } from '../../../store/policy_details/selectors'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; + +export const PolicyHostIsolationExceptionsList = ({ + exceptions, + policyId, +}: { + exceptions: FoundExceptionListItemSchema; + policyId: string; +}) => { + const history = useHistory(); + // load the list of policies> + const policiesRequest = useGetEndpointSpecificPolicies(); + const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation); + + const [expandedItemsMap, setExpandedItemsMap] = useState>(new Map()); + + const pagination = { + totalItemCount: exceptions?.total ?? 0, + pageSize: exceptions?.per_page ?? MANAGEMENT_DEFAULT_PAGE_SIZE, + pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS], + pageIndex: (exceptions?.page ?? 1) - 1, + }; + + const handlePageChange = useCallback( + ({ pageIndex, pageSize }) => { + history.push( + getPolicyHostIsolationExceptionsPath(policyId, { + ...urlParams, + // If user changed page size, then reset page index back to the first page + page_index: pageIndex, + page_size: pageSize, + }) + ); + }, + [history, policyId, urlParams] + ); + + const handleSearchInput = useCallback( + (filter: string) => { + history.push( + getPolicyHostIsolationExceptionsPath(policyId, { + ...urlParams, + filter, + }) + ); + }, + [history, policyId, urlParams] + ); + + const artifactCardPolicies = useEndpointPoliciesToArtifactPolicies(policiesRequest.data?.items); + + const provideCardProps: ArtifactCardGridProps['cardComponentProps'] = (item) => { + return { + expanded: expandedItemsMap.get(item.id) || false, + actions: [], + policies: artifactCardPolicies, + }; + }; + + const handleExpandCollapse: ArtifactCardGridProps['onExpandCollapse'] = ({ + expanded, + collapsed, + }) => { + const newExpandedMap = new Map(expandedItemsMap); + for (const item of expanded) { + newExpandedMap.set(item.id, true); + } + for (const item of collapsed) { + newExpandedMap.set(item.id, false); + } + setExpandedItemsMap(newExpandedMap); + }; + + const totalItemsCountLabel = useMemo(() => { + return i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.totalItemCount', + { + defaultMessage: 'Showing {totalItemsCount, plural, one {# exception} other {# exceptions}}', + values: { totalItemsCount: pagination.totalItemCount }, + } + ); + }, [pagination.totalItemCount]); + + return ( + <> + + + + {totalItemsCountLabel} + + + + + ); +}; +PolicyHostIsolationExceptionsList.displayName = 'PolicyHostIsolationExceptionsList'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.test.tsx new file mode 100644 index 0000000000000..3ccb4ea4d445e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.test.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { getFoundExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; +import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; +import { PolicyData } from '../../../../../../common/endpoint/types'; +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../common/mock/endpoint'; +import { getPolicyHostIsolationExceptionsPath } from '../../../../common/routing'; +import { getHostIsolationExceptionItems } from '../../../host_isolation_exceptions/service'; +import { PolicyHostIsolationExceptionsTab } from './host_isolation_exceptions_tab'; + +jest.mock('../../../host_isolation_exceptions/service'); + +const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock; + +const endpointGenerator = new EndpointDocGenerator('seed'); + +const emptyList = { + data: [], + page: 1, + per_page: 10, + total: 0, +}; + +describe('Policy details host isolation exceptions tab', () => { + let policyId: string; + let policy: PolicyData; + let render: () => ReturnType; + let renderResult: ReturnType; + let history: AppContextTestRender['history']; + let mockedContext: AppContextTestRender; + + beforeEach(() => { + getHostIsolationExceptionItemsMock.mockClear(); + policy = endpointGenerator.generatePolicyPackagePolicy(); + policyId = policy.id; + mockedContext = createAppRootMockRenderer(); + ({ history } = mockedContext); + render = () => + (renderResult = mockedContext.render()); + + history.push(getPolicyHostIsolationExceptionsPath(policyId)); + }); + + it('should display display a "loading" state while requests happen', async () => { + const promises: Array<() => void> = []; + getHostIsolationExceptionItemsMock.mockImplementation(() => { + return new Promise((resolve) => promises.push(resolve)); + }); + render(); + expect(await renderResult.findByTestId('policyHostIsolationExceptionsTabLoading')).toBeTruthy(); + // prevent memory leaks + promises.forEach((resolve) => resolve()); + }); + + it("should display an 'unexistent' empty state if there are no host isolation exceptions at all", async () => { + // mock no data for all requests + getHostIsolationExceptionItemsMock.mockResolvedValue({ + ...emptyList, + }); + render(); + expect( + await renderResult.findByTestId('policy-host-isolation-exceptions-empty-unexisting') + ).toBeTruthy(); + }); + + it("should display an 'unassigned' empty state if there are no host isolation exceptions assigned", async () => { + // mock no data for all requests + getHostIsolationExceptionItemsMock.mockImplementation((params) => { + // no filter = fetch all exceptions + if (!params.filter) { + return { + ...emptyList, + total: 1, + }; + } + return { + ...emptyList, + }; + }); + render(); + expect( + await renderResult.findByTestId('policy-host-isolation-exceptions-empty-unassigned') + ).toBeTruthy(); + }); + + it('Should display the count of total assigned policies', async () => { + getHostIsolationExceptionItemsMock.mockImplementation(() => { + return getFoundExceptionListItemSchemaMock(4); + }); + render(); + expect( + await renderResult.findByTestId('policyHostIsolationExceptionsTabSubtitle') + ).toHaveTextContent('There are 4 exceptions associated with this policy'); + }); + + it('should apply a filter when requested from location search params', async () => { + history.push(getPolicyHostIsolationExceptionsPath(policyId, { filter: 'my filter' })); + getHostIsolationExceptionItemsMock.mockImplementation(() => { + return getFoundExceptionListItemSchemaMock(4); + }); + render(); + expect(getHostIsolationExceptionItemsMock).toHaveBeenLastCalledWith({ + filter: `((exception-list-agnostic.attributes.tags:"policy:${policyId}" OR exception-list-agnostic.attributes.tags:"policy:all")) AND ((exception-list-agnostic.attributes.name:(*my*filter*) OR exception-list-agnostic.attributes.description:(*my*filter*) OR exception-list-agnostic.attributes.entries.value:(*my*filter*)))`, + http: mockedContext.coreStart.http, + page: 1, + perPage: 10, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.tsx new file mode 100644 index 0000000000000..c2df907917c5e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/host_isolation_exceptions_tab.tsx @@ -0,0 +1,164 @@ +/* + * 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 { + EuiLink, + EuiPageContent, + EuiPageHeader, + EuiPageHeaderSection, + EuiProgress, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useMemo } from 'react'; +import { APP_UI_ID } from '../../../../../../common/constants'; +import { PolicyData } from '../../../../../../common/endpoint/types'; +import { useAppUrl } from '../../../../../common/lib/kibana'; +import { + MANAGEMENT_DEFAULT_PAGE, + MANAGEMENT_DEFAULT_PAGE_SIZE, +} from '../../../../common/constants'; +import { getHostIsolationExceptionsListPath } from '../../../../common/routing'; +import { useFetchHostIsolationExceptionsList } from '../../../host_isolation_exceptions/view/hooks'; +import { getCurrentArtifactsLocation } from '../../store/policy_details/selectors'; +import { usePolicyDetailsSelector } from '../policy_hooks'; +import { PolicyHostIsolationExceptionsEmptyUnexisting } from './components/empty_unexisting'; +import { PolicyHostIsolationExceptionsEmptyUnassigned } from './components/empty_unassigned'; +import { PolicyHostIsolationExceptionsList } from './components/list'; + +export const PolicyHostIsolationExceptionsTab = ({ policy }: { policy: PolicyData }) => { + const { getAppUrl } = useAppUrl(); + + const policyId = policy.id; + + const location = usePolicyDetailsSelector(getCurrentArtifactsLocation); + const toHostIsolationList = getAppUrl({ + appId: APP_UI_ID, + path: getHostIsolationExceptionsListPath(), + }); + + const allPolicyExceptionsListRequest = useFetchHostIsolationExceptionsList({ + page: MANAGEMENT_DEFAULT_PAGE, + perPage: MANAGEMENT_DEFAULT_PAGE_SIZE, + policies: [policyId, 'all'], + }); + + const policySearchedExceptionsListRequest = useFetchHostIsolationExceptionsList({ + filter: location.filter, + page: location.page_index, + perPage: location.page_size, + policies: [policyId, 'all'], + }); + + const allExceptionsListRequest = useFetchHostIsolationExceptionsList({ + page: MANAGEMENT_DEFAULT_PAGE, + perPage: MANAGEMENT_DEFAULT_PAGE_SIZE, + // only do this request if no assigned policies found + enabled: allPolicyExceptionsListRequest.data?.total === 0, + }); + + const hasNoAssignedOrExistingExceptions = allPolicyExceptionsListRequest.data?.total === 0; + const hasNoExistingExceptions = allExceptionsListRequest.data?.total === 0; + + const subTitle = useMemo(() => { + const link = ( + + + + ); + + return policySearchedExceptionsListRequest.data ? ( + + ) : null; + }, [ + allPolicyExceptionsListRequest.data?.total, + getAppUrl, + policySearchedExceptionsListRequest.data, + toHostIsolationList, + ]); + + const isLoading = + policySearchedExceptionsListRequest.isLoading || + allPolicyExceptionsListRequest.isLoading || + allExceptionsListRequest.isLoading || + !policy; + + // render non-existent or non-assigned messages + if (!isLoading && (hasNoAssignedOrExistingExceptions || hasNoExistingExceptions)) { + if (hasNoExistingExceptions) { + return ( + + ); + } else { + return ( + + ); + } + } + + // render header and list + return !isLoading && policySearchedExceptionsListRequest.data ? ( +
+ + + +

+ {i18n.translate( + 'xpack.securitySolution.endpoint.policy.hostIsolationExceptions.list.title', + { + defaultMessage: 'Assigned host isolation exceptions', + } + )} +

+
+ + + + +

{subTitle}

+
+
+
+ + + + + +
+ ) : ( + + ); +}; +PolicyHostIsolationExceptionsTab.displayName = 'PolicyHostIsolationExceptionsTab'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 4159b1ed62f65..fa99d72523531 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -127,6 +127,7 @@ describe('Policy Details', () => { expect(pageTitle).toHaveLength(1); expect(pageTitle.text()).toEqual(policyPackagePolicy.name); }); + it('should navigate to list if back to link is clicked', async () => { policyView.update(); @@ -135,6 +136,7 @@ describe('Policy Details', () => { backToListLink.simulate('click', { button: 0 }); expect(history.location.pathname).toEqual(endpointListPath); }); + it('should display agent stats', async () => { await asyncActions; policyView.update(); @@ -143,6 +145,7 @@ describe('Policy Details', () => { expect(agentsSummary).toHaveLength(1); expect(agentsSummary.text()).toBe('Total agents5Healthy3Unhealthy1Offline1'); }); + it('should display event filters tab', async () => { await asyncActions; policyView.update(); @@ -151,5 +154,11 @@ describe('Policy Details', () => { expect(eventFiltersTab).toHaveLength(1); expect(eventFiltersTab.text()).toBe('Event filters'); }); + + it('should display the host isolation exceptions tab', async () => { + await asyncActions; + policyView.update(); + expect(policyView.find('#hostIsolationExceptions')).toBeTruthy(); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx index efb113f38df1c..e2c2ef939603e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx @@ -5,34 +5,37 @@ * 2.0. */ +import { EuiSpacer, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React, { useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { EuiTabbedContent, EuiSpacer, EuiTabbedContentTab } from '@elastic/eui'; - -import { usePolicyDetailsSelector } from '../policy_hooks'; +import { PolicyData } from '../../../../../../common/endpoint/types'; import { + getPolicyDetailPath, + getPolicyEventFiltersPath, + getPolicyHostIsolationExceptionsPath, + getPolicyTrustedAppsPath, +} from '../../../../common/routing'; +import { + isOnHostIsolationExceptionsView, + isOnPolicyEventFiltersView, isOnPolicyFormView, isOnPolicyTrustedAppsView, - isOnPolicyEventFiltersView, - policyIdFromParams, policyDetails, + policyIdFromParams, } from '../../store/policy_details/selectors'; - -import { PolicyTrustedAppsLayout } from '../trusted_apps/layout'; import { PolicyEventFiltersLayout } from '../event_filters/layout'; +import { PolicyHostIsolationExceptionsTab } from '../host_isolation_exceptions/host_isolation_exceptions_tab'; import { PolicyFormLayout } from '../policy_forms/components'; -import { - getPolicyDetailPath, - getPolicyTrustedAppsPath, - getPolicyEventFiltersPath, -} from '../../../../common/routing'; +import { usePolicyDetailsSelector } from '../policy_hooks'; +import { PolicyTrustedAppsLayout } from '../trusted_apps/layout'; export const PolicyTabs = React.memo(() => { const history = useHistory(); const isInSettingsTab = usePolicyDetailsSelector(isOnPolicyFormView); const isInTrustedAppsTab = usePolicyDetailsSelector(isOnPolicyTrustedAppsView); const isInEventFilters = usePolicyDetailsSelector(isOnPolicyEventFiltersView); + const isInHostIsolationExceptionsTab = usePolicyDetailsSelector(isOnHostIsolationExceptionsView); const policyId = usePolicyDetailsSelector(policyIdFromParams); const policyItem = usePolicyDetailsSelector(policyDetails); @@ -74,6 +77,21 @@ export const PolicyTabs = React.memo(() => { ), }, + { + id: 'hostIsolationExceptions', + name: i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.tabs.isInHostIsolationExceptions', + { + defaultMessage: 'Host isolation exceptions', + } + ), + content: ( + <> + + + + ), + }, ], [policyItem] ); @@ -87,19 +105,30 @@ export const PolicyTabs = React.memo(() => { initialTab = tabs[1]; } else if (isInEventFilters) { initialTab = tabs[2]; + } else if (isInHostIsolationExceptionsTab) { + initialTab = tabs[3]; } return initialTab; - }, [isInSettingsTab, isInTrustedAppsTab, isInEventFilters, tabs]); + }, [isInSettingsTab, isInTrustedAppsTab, isInEventFilters, isInHostIsolationExceptionsTab, tabs]); const onTabClickHandler = useCallback( (selectedTab: EuiTabbedContentTab) => { - const path = - selectedTab.id === 'settings' - ? getPolicyDetailPath(policyId) - : selectedTab.id === 'trustedApps' - ? getPolicyTrustedAppsPath(policyId) - : getPolicyEventFiltersPath(policyId); + let path: string = ''; + switch (selectedTab.id) { + case 'settings': + path = getPolicyDetailPath(policyId); + break; + case 'trustedApps': + path = getPolicyTrustedAppsPath(policyId); + break; + case 'hostIsolationExceptions': + path = getPolicyHostIsolationExceptionsPath(policyId); + break; + case 'eventFilters': + path = getPolicyEventFiltersPath(policyId); + break; + } history.push(path); }, [history, policyId] diff --git a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts index d59b2ca984131..1a0c7ec74d451 100644 --- a/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/services/policies/hooks.ts @@ -13,8 +13,8 @@ import { sendGetEndpointSpecificPackagePolicies } from './policies'; export function useGetEndpointSpecificPolicies({ onError, }: { - onError: (error: ServerApiError) => void; -}): QueryObserverResult { + onError?: (error: ServerApiError) => void; +} = {}): QueryObserverResult { const http = useHttp(); return useQuery( ['endpointSpecificPolicies'], From 43654e95451d16ef1fa8be31297e9eee578319f8 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 13 Dec 2021 17:25:49 +0100 Subject: [PATCH 036/145] wait for vis before asserting on it (#121083) --- x-pack/test/functional/apps/lens/smokescreen.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 7cacee6446723..7064b11113fad 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -59,6 +59,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await listingTable.searchForItemWithName('Afancilenstest'); await PageObjects.lens.clickVisualizeListItemTitle('Afancilenstest'); await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.waitForVisualization(); expect(await PageObjects.lens.getTitle()).to.eql('Afancilenstest'); @@ -80,6 +81,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { keepOpen: true, }); await PageObjects.lens.addFilterToAgg(`geo.src : CN`); + await PageObjects.lens.waitForVisualization(); // Verify that the field was persisted from the transition expect(await PageObjects.lens.getFiltersAggLabels()).to.eql([`ip : *`, `geo.src : CN`]); From 93cac0ca03c4b4be3372bb849fae33d805e96a03 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 13 Dec 2021 09:29:41 -0700 Subject: [PATCH 037/145] =?UTF-8?q?[maps]=20fix=20flaky=20test=20functiona?= =?UTF-8?q?l/apps/maps/embeddable/dashboard=C2=B7js=20(#120771)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * unskip * .only * add retry around failing section * waitForLayersToLoad * remove only * eslint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../functional/apps/maps/embeddable/dashboard.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/embeddable/dashboard.js index c92567e246736..a670ebdb4ea23 100644 --- a/x-pack/test/functional/apps/maps/embeddable/dashboard.js +++ b/x-pack/test/functional/apps/maps/embeddable/dashboard.js @@ -18,8 +18,7 @@ export default function ({ getPageObjects, getService }) { const retry = getService('retry'); const security = getService('security'); - // FLAKY: https://github.com/elastic/kibana/issues/113993 - describe.skip('embed in dashboard', () => { + describe('embed in dashboard', () => { before(async () => { await security.testUser.setRoles( [ @@ -96,8 +95,15 @@ export default function ({ getPageObjects, getService }) { it('should apply new container state (time, query, filters) to embeddable', async () => { await filterBar.selectIndexPattern('logstash-*'); await filterBar.addFilter('machine.os', 'is', 'win 8'); - await filterBar.selectIndexPattern('meta_for_geo_shapes*'); - await filterBar.addFilter('shape_name', 'is', 'alpha'); // runtime fields do not have autocomplete + await PageObjects.maps.waitForLayersToLoad(); + + // retry is fix for flaky test https://github.com/elastic/kibana/issues/113993 + // timing issue where click for addFilter opens filter pill created above instead of clicking addFilter + await retry.try(async () => { + await filterBar.selectIndexPattern('meta_for_geo_shapes*'); + await filterBar.addFilter('shape_name', 'is', 'alpha'); // runtime fields do not have autocomplete + }); + await PageObjects.maps.waitForLayersToLoad(); const { rawResponse: gridResponse } = await PageObjects.maps.getResponseFromDashboardPanel( 'geo grid vector grid example' From 8550977c321a5d5302acf29d03b1a1e26970a4d7 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 13 Dec 2021 09:31:09 -0700 Subject: [PATCH 038/145] =?UTF-8?q?[maps]=20fix=20test/functional/apps/map?= =?UTF-8?q?s/embeddable/tooltip=5Ffilter=5Factions=C2=B7js=20(#120854)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [maps] fix test/functional/apps/maps/embeddable/tooltip_filter_actions·js * add method to wait for layers with collapsed layer control * type * unskip flaky test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/layer_control.test.tsx.snap | 4 ++++ .../right_side_controls/layer_control/layer_control.tsx | 2 ++ .../apps/maps/embeddable/tooltip_filter_actions.js | 7 ++++--- x-pack/test/functional/page_objects/gis_page.ts | 8 ++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/__snapshots__/layer_control.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/__snapshots__/layer_control.test.tsx.snap index 047f0087c559f..7d0c67ff41797 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/__snapshots__/layer_control.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/__snapshots__/layer_control.test.tsx.snap @@ -89,6 +89,7 @@ exports[`LayerControl isLayerTOCOpen Should render expand button 1`] = ` aria-label="Expand layers panel" className="mapLayerControl__openLayerTOCButton" color="text" + data-test-subj="mapExpandLayerControlButton" iconType="menuLeft" onClick={[Function]} /> @@ -106,6 +107,7 @@ exports[`LayerControl isLayerTOCOpen Should render expand button with error icon aria-label="Expand layers panel" className="mapLayerControl__openLayerTOCButton" color="text" + data-test-subj="mapExpandLayerControlButton" iconType="alert" onClick={[Function]} /> @@ -123,6 +125,7 @@ exports[`LayerControl isLayerTOCOpen spinner icon Should not render expand butto aria-label="Expand layers panel" className="mapLayerControl__openLayerTOCButton" color="text" + data-test-subj="mapExpandLayerControlButton" iconType="menuLeft" onClick={[Function]} /> @@ -139,6 +142,7 @@ exports[`LayerControl isLayerTOCOpen spinner icon Should render expand button wi @@ -66,6 +67,7 @@ function renderExpandButton({ onClick={onClick} iconType={hasErrors ? 'alert' : 'menuLeft'} aria-label={expandLabel} + data-test-subj="mapExpandLayerControlButton" /> ); } diff --git a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js index 400c687d6af88..c3c014447a36d 100644 --- a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js +++ b/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { - const PageObjects = getPageObjects(['common', 'dashboard', 'discover', 'maps']); + const PageObjects = getPageObjects(['common', 'dashboard', 'discover', 'header', 'maps']); const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); @@ -41,8 +41,7 @@ export default function ({ getPageObjects, getService }) { await security.testUser.restoreDefaults(); }); - // FLAKY: https://github.com/elastic/kibana/issues/117780 - describe.skip('apply filter to current view', () => { + describe('apply filter to current view', () => { before(async () => { await loadDashboardAndOpenTooltip(); }); @@ -54,6 +53,8 @@ export default function ({ getPageObjects, getService }) { it('should create filters when create filter button is clicked', async () => { await testSubjects.click('mapTooltipCreateFilterButton'); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + await PageObjects.maps.waitForLayersToLoadMinimizedLayerControl(); const numFilters = await filterBar.getFilterCount(); expect(numFilters).to.be(1); diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index 629402a0a4768..89b80cf4ccf95 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -106,6 +106,14 @@ export class GisPageObject extends FtrService { }); } + async waitForLayersToLoadMinimizedLayerControl() { + this.log.debug('Wait for layers to load (minimized layer control)'); + await this.retry.try(async () => { + const tableOfContents = await this.testSubjects.find('mapExpandLayerControlButton'); + await tableOfContents.waitForDeletedByCssSelector('.euiLoadingSpinner'); + }); + } + async waitForLayerDeleted(layerName: string) { this.log.debug('Wait for layer deleted'); await this.retry.waitFor('Layer to be deleted', async () => { From 5a9d4cb35c6e7d85b02fe784b0120e2cd6c13d10 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 13 Dec 2021 09:32:39 -0700 Subject: [PATCH 039/145] =?UTF-8?q?[maps]=20fix=20flaky=20test=20/function?= =?UTF-8?q?al/apps/maps/add=5Flayer=5Fpanel=C2=B7js=20(#120990)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [maps] fix flaky test /functional/apps/maps/add_layer_panel·js * eslint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../geo_index_pattern_select.test.tsx.snap | 2 ++ .../components/geo_index_pattern_select.tsx | 1 + .../functional/apps/maps/add_layer_panel.js | 19 ++++++------------- .../test/functional/page_objects/gis_page.ts | 12 +++++++++++- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/maps/public/components/__snapshots__/geo_index_pattern_select.test.tsx.snap b/x-pack/plugins/maps/public/components/__snapshots__/geo_index_pattern_select.test.tsx.snap index 070000a2f6b98..fc10eceb45601 100644 --- a/x-pack/plugins/maps/public/components/__snapshots__/geo_index_pattern_select.test.tsx.snap +++ b/x-pack/plugins/maps/public/components/__snapshots__/geo_index_pattern_select.test.tsx.snap @@ -14,6 +14,7 @@ exports[`should render 1`] = ` labelType="label" > { placeholder={getDataViewSelectPlaceholder()} onNoIndexPatterns={this._onNoIndexPatterns} isClearable={false} + data-test-subj="mapGeoIndexPatternSelect" /> diff --git a/x-pack/test/functional/apps/maps/add_layer_panel.js b/x-pack/test/functional/apps/maps/add_layer_panel.js index 37c4c2c9b14c6..b11694679e172 100644 --- a/x-pack/test/functional/apps/maps/add_layer_panel.js +++ b/x-pack/test/functional/apps/maps/add_layer_panel.js @@ -13,14 +13,12 @@ export default function ({ getService, getPageObjects }) { const security = getService('security'); describe('Add layer panel', () => { - const LAYER_NAME = 'World Countries'; - before(async () => { - await security.testUser.setRoles(['global_maps_all']); + await security.testUser.setRoles(['global_maps_all', 'test_logstash_reader']); await PageObjects.maps.openNewMap(); await PageObjects.maps.clickAddLayer(); - await PageObjects.maps.selectEMSBoundariesSource(); - await PageObjects.maps.selectVectorLayer(LAYER_NAME); + await PageObjects.maps.selectDocumentsSource(); + await PageObjects.maps.selectGeoIndexPatternLayer('logstash-*'); }); after(async () => { @@ -28,7 +26,7 @@ export default function ({ getService, getPageObjects }) { }); it('should show unsaved layer in layer TOC', async () => { - const vectorLayerExists = await PageObjects.maps.doesLayerExist(LAYER_NAME); + const vectorLayerExists = await PageObjects.maps.doesLayerExist('logstash-*'); expect(vectorLayerExists).to.be(true); }); @@ -36,17 +34,12 @@ export default function ({ getService, getPageObjects }) { const mapSaveButton = await testSubjects.find('mapSaveButton'); const isDisabled = await mapSaveButton.getAttribute('disabled'); expect(isDisabled).to.be('true'); - - const panelOpen = await PageObjects.maps.isLayerAddPanelOpen(); - expect(panelOpen).to.be(true); - const vectorLayerExists = await PageObjects.maps.doesLayerExist(LAYER_NAME); - expect(vectorLayerExists).to.be(true); }); it('should remove layer on cancel', async () => { - await PageObjects.maps.cancelLayerAdd(LAYER_NAME); + await PageObjects.maps.cancelLayerAdd('logstash-*'); - const vectorLayerExists = await PageObjects.maps.doesLayerExist(LAYER_NAME); + const vectorLayerExists = await PageObjects.maps.doesLayerExist('logstash-*'); expect(vectorLayerExists).to.be(false); }); }); diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index 89b80cf4ccf95..c0e035e4ec4e0 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -529,6 +529,17 @@ export class GisPageObject extends FtrService { await this.waitForLayersToLoad(); } + async selectDocumentsSource() { + this.log.debug(`Select Documents source`); + await this.testSubjects.click('documents'); + } + + async selectGeoIndexPatternLayer(name: string) { + this.log.debug(`Select index pattern ${name}`); + await this.comboBox.set('mapGeoIndexPatternSelect', name); + await this.waitForLayersToLoad(); + } + async selectEMSBoundariesSource() { this.log.debug(`Select Elastic Maps Service boundaries source`); await this.testSubjects.click('emsBoundaries'); @@ -547,7 +558,6 @@ export class GisPageObject extends FtrService { await this.waitForLayersToLoad(); } - // Returns first layer by default async selectVectorLayer(vectorLayerName: string) { this.log.debug(`Select EMS vector layer ${vectorLayerName}`); if (!vectorLayerName) { From 322e2a48aa75700dc132b8dc217e5733f3fd3473 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 13 Dec 2021 17:34:01 +0100 Subject: [PATCH 040/145] [Cases] Fix flaky test on form submit (#121073) * change to use simulate click for submitting * lint * type check --- .../public/components/create/form.test.tsx | 48 +++++++------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index 9f1e2e6c6dda3..9a4af9bdc3a62 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -7,14 +7,14 @@ import React from 'react'; import { mount } from 'enzyme'; -import { act, render, waitFor } from '@testing-library/react'; +import { act, render } from '@testing-library/react'; import { useForm, Form, FormHook } from '../../common/shared_imports'; import { useGetTags } from '../../containers/use_get_tags'; import { useConnectors } from '../../containers/configure/use_connectors'; import { connectorsMock } from '../../containers/mock'; import { schema, FormProps } from './schema'; -import { CreateCaseForm, CreateCaseFormFields, CreateCaseFormProps } from './form'; +import { CreateCaseForm, CreateCaseFormProps } from './form'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { useCaseConfigureResponse } from '../configure_cases/__mock__'; import { TestProviders } from '../../common/mock'; @@ -114,34 +114,22 @@ describe('CreateCaseForm', () => { expect(queryByText('Sync alert')).not.toBeInTheDocument(); }); - describe('CreateCaseFormFields', () => { - it('should render spinner when loading', async () => { - const wrapper = mount( - - - - ); - - await act(async () => { - globalForm.setFieldValue('title', 'title'); - globalForm.setFieldValue('description', 'description'); - globalForm.submit(); - // For some weird reason this is needed to pass the test. - // It does not do anything useful - await wrapper.find(`[data-test-subj="caseTitle"]`); - await wrapper.update(); - }); - - await waitFor(() => { - expect( - wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists() - ).toBeTruthy(); - }); + it('should render spinner when loading', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="create-case-submit"]`).exists()).toBeTruthy(); + + await act(async () => { + globalForm.setFieldValue('title', 'title'); + globalForm.setFieldValue('description', 'description'); + await wrapper.find(`button[data-test-subj="create-case-submit"]`).simulate('click'); + wrapper.update(); }); + + expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy(); }); }); From 9b5efb27335a30eae265c626fe45628ca8a003e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Mon, 13 Dec 2021 11:39:24 -0500 Subject: [PATCH 041/145] [APM] Add comparision to service maps popover (#120839) * adding comparison to service maps * adding tests * addressing pr comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../service_map/Popover/backend_contents.tsx | 27 ++- .../service_map/Popover/service_contents.tsx | 29 ++- .../app/service_map/Popover/stats_list.tsx | 122 ++++++++----- .../components/app/service_map/index.tsx | 2 +- .../get_service_map_backend_node_info.ts | 28 ++- .../get_service_map_service_node_info.ts | 34 +++- .../apm/server/routes/service_map/route.ts | 39 ++-- .../tests/service_maps/service_maps.spec.ts | 166 +++++++++++++++--- 8 files changed, 334 insertions(+), 113 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx index 8fa93e22a90fe..a1e4cbe67e65c 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx @@ -12,12 +12,20 @@ import { METRIC_TYPE } from '@kbn/analytics'; import React from 'react'; import { useUiTracker } from '../../../../../../observability/public'; import { ContentsProps } from '.'; -import { NodeStats } from '../../../../../common/service_map'; import { useAnyOfApmParams } from '../../../../hooks/use_apm_params'; import { useApmRouter } from '../../../../hooks/use_apm_router'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { ApmRoutes } from '../../../routing/apm_route_config'; import { StatsList } from './stats_list'; +import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; + +type BackendReturn = APIReturnType<'GET /internal/apm/service-map/backend'>; + +const INITIAL_STATE: Partial = { + currentPeriod: undefined, + previousPeriod: undefined, +}; export function BackendContents({ nodeData, @@ -30,11 +38,20 @@ export function BackendContents({ '/services/{serviceName}/service-map' ); + const { comparisonEnabled, comparisonType } = query; + + const { offset } = getTimeRangeComparison({ + start, + end, + comparisonEnabled, + comparisonType, + }); + const apmRouter = useApmRouter(); const backendName = nodeData.label; - const { data = { transactionStats: {} } as NodeStats, status } = useFetcher( + const { data = INITIAL_STATE, status } = useFetcher( (callApmApi) => { if (backendName) { return callApmApi({ @@ -45,15 +62,13 @@ export function BackendContents({ environment, start, end, + offset, }, }, }); } }, - [environment, backendName, start, end], - { - preservePreviousData: false, - } + [environment, backendName, start, end, offset] ); const isLoading = status === FETCH_STATUS.LOADING; diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx index 10d558e648376..b0ca933e64819 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx @@ -17,12 +17,21 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useApmParams } from '../../../../hooks/use_apm_params'; import type { ContentsProps } from '.'; -import { NodeStats } from '../../../../../common/service_map'; import { useApmRouter } from '../../../../hooks/use_apm_router'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { AnomalyDetection } from './anomaly_detection'; import { StatsList } from './stats_list'; import { useTimeRange } from '../../../../hooks/use_time_range'; +import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; + +type ServiceNodeReturn = + APIReturnType<'GET /internal/apm/service-map/service/{serviceName}'>; + +const INITIAL_STATE: ServiceNodeReturn = { + currentPeriod: {}, + previousPeriod: undefined, +}; export function ServiceContents({ onFocusClick, @@ -42,28 +51,32 @@ export function ServiceContents({ throw new Error('Expected rangeFrom and rangeTo to be set'); } - const { rangeFrom, rangeTo } = query; + const { rangeFrom, rangeTo, comparisonEnabled, comparisonType } = query; const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + const { offset } = getTimeRangeComparison({ + start, + end, + comparisonEnabled, + comparisonType, + }); + const serviceName = nodeData.id!; - const { data = { transactionStats: {} } as NodeStats, status } = useFetcher( + const { data = INITIAL_STATE, status } = useFetcher( (callApmApi) => { if (serviceName && start && end) { return callApmApi({ endpoint: 'GET /internal/apm/service-map/service/{serviceName}', params: { path: { serviceName }, - query: { environment, start, end }, + query: { environment, start, end, offset }, }, }); } }, - [environment, serviceName, start, end], - { - preservePreviousData: false, - } + [environment, serviceName, start, end, offset] ); const isLoading = status === FETCH_STATUS.LOADING; diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx index 002c480503454..1b8e1f64859f4 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx @@ -14,15 +14,18 @@ import { import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import React, { useMemo } from 'react'; -import { NodeStats } from '../../../../../common/service_map'; import { asDuration, asPercent, asTransactionRate, } from '../../../../../common/utils/formatters'; import { Coordinate } from '../../../../../typings/timeseries'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { SparkPlot, Color } from '../../../shared/charts/spark_plot'; +type ServiceNodeReturn = + APIReturnType<'GET /internal/apm/service-map/service/{serviceName}'>; + function LoadingSpinner() { return ( ; } interface Item { title: string; valueLabel: string | null; timeseries?: Coordinate[]; + previousPeriodTimeseries?: Coordinate[]; color: Color; } export function StatsList({ data, isLoading }: StatsListProps) { + const { currentPeriod = {}, previousPeriod } = data; const { cpuUsage, failedTransactionsRate, memoryUsage, transactionStats } = - data; + currentPeriod; const hasData = [ cpuUsage?.value, @@ -78,10 +83,10 @@ export function StatsList({ data, isLoading }: StatsListProps) { defaultMessage: 'Latency (avg.)', } ), - valueLabel: isNumber(transactionStats?.latency?.value) - ? asDuration(transactionStats?.latency?.value) - : null, - timeseries: transactionStats?.latency?.timeseries, + valueLabel: asDuration(currentPeriod?.transactionStats?.latency?.value), + timeseries: currentPeriod?.transactionStats?.latency?.timeseries, + previousPeriodTimeseries: + previousPeriod?.transactionStats?.latency?.timeseries, color: 'euiColorVis1', }, { @@ -91,24 +96,35 @@ export function StatsList({ data, isLoading }: StatsListProps) { defaultMessage: 'Throughput (avg.)', } ), - valueLabel: asTransactionRate(transactionStats?.throughput?.value), - timeseries: transactionStats?.throughput?.timeseries, + valueLabel: asTransactionRate( + currentPeriod?.transactionStats?.throughput?.value + ), + timeseries: currentPeriod?.transactionStats?.throughput?.timeseries, + previousPeriodTimeseries: + previousPeriod?.transactionStats?.throughput?.timeseries, color: 'euiColorVis0', }, { title: i18n.translate('xpack.apm.serviceMap.errorRatePopoverStat', { defaultMessage: 'Failed transaction rate (avg.)', }), - valueLabel: asPercent(failedTransactionsRate?.value, 1, ''), - timeseries: failedTransactionsRate?.timeseries, + valueLabel: asPercent( + currentPeriod?.failedTransactionsRate?.value, + 1, + '' + ), + timeseries: currentPeriod?.failedTransactionsRate?.timeseries, + previousPeriodTimeseries: + previousPeriod?.failedTransactionsRate?.timeseries, color: 'euiColorVis7', }, { title: i18n.translate('xpack.apm.serviceMap.avgCpuUsagePopoverStat', { defaultMessage: 'CPU usage (avg.)', }), - valueLabel: asPercent(cpuUsage?.value, 1, ''), - timeseries: cpuUsage?.timeseries, + valueLabel: asPercent(currentPeriod?.cpuUsage?.value, 1, ''), + timeseries: currentPeriod?.cpuUsage?.timeseries, + previousPeriodTimeseries: previousPeriod?.cpuUsage?.timeseries, color: 'euiColorVis3', }, { @@ -118,15 +134,16 @@ export function StatsList({ data, isLoading }: StatsListProps) { defaultMessage: 'Memory usage (avg.)', } ), - valueLabel: asPercent(memoryUsage?.value, 1, ''), - timeseries: memoryUsage?.timeseries, + valueLabel: asPercent(currentPeriod?.memoryUsage?.value, 1, ''), + timeseries: currentPeriod?.memoryUsage?.timeseries, + previousPeriodTimeseries: previousPeriod?.memoryUsage?.timeseries, color: 'euiColorVis8', }, ], - [cpuUsage, failedTransactionsRate, memoryUsage, transactionStats] + [currentPeriod, previousPeriod] ); - if (isLoading) { + if (isLoading && !hasData) { return ; } @@ -136,38 +153,47 @@ export function StatsList({ data, isLoading }: StatsListProps) { return ( - {items.map(({ title, valueLabel, timeseries, color }) => { - if (!valueLabel) { - return null; + {items.map( + ({ + title, + valueLabel, + timeseries, + color, + previousPeriodTimeseries, + }) => { + if (!valueLabel) { + return null; + } + return ( + + + + + {title} + + + + {timeseries ? ( + + ) : ( +
{valueLabel}
+ )} +
+
+
+ ); } - return ( - - - - - {title} - - - - {timeseries ? ( - - ) : ( -
{valueLabel}
- )} -
-
-
- ); - })} + )}
); } diff --git a/x-pack/plugins/apm/public/components/app/service_map/index.tsx b/x-pack/plugins/apm/public/components/app/service_map/index.tsx index 0ec1e6630003a..ff19029243d07 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/index.tsx @@ -180,7 +180,7 @@ export function ServiceMap({ return ( <> - +
{ return withApmSpan('get_service_map_backend_node_stats', async () => { const { apmEventClient } = setup; + const { offsetInMs, startWithOffset, endWithOffset } = getOffsetInMs({ + start, + end, + offset, + }); - const { intervalString } = getBucketSize({ start, end, numBuckets: 20 }); + const { intervalString } = getBucketSize({ + start: startWithOffset, + end: endWithOffset, + numBuckets: 20, + }); const subAggs = { latency_sum: { @@ -66,7 +78,7 @@ export function getServiceMapBackendNodeInfo({ bool: { filter: [ { term: { [SPAN_DESTINATION_SERVICE_RESOURCE]: backendName } }, - ...rangeQuery(start, end), + ...rangeQuery(startWithOffset, endWithOffset), ...environmentQuery(environment), ], }, @@ -78,7 +90,7 @@ export function getServiceMapBackendNodeInfo({ field: '@timestamp', fixed_interval: intervalString, min_doc_count: 0, - extended_bounds: { min: start, max: end }, + extended_bounds: { min: startWithOffset, max: endWithOffset }, }, aggs: subAggs, }, @@ -95,8 +107,8 @@ export function getServiceMapBackendNodeInfo({ const avgFailedTransactionsRate = failedTransactionsRateCount / count; const latency = latencySum / count; const throughput = calculateThroughputWithRange({ - start, - end, + start: startWithOffset, + end: endWithOffset, value: count, }); @@ -116,7 +128,7 @@ export function getServiceMapBackendNodeInfo({ timeseries: response.aggregations?.timeseries ? getFailedTransactionRateTimeSeries( response.aggregations.timeseries.buckets - ) + ).map(({ x, y }) => ({ x: x + offsetInMs, y })) : undefined, }, transactionStats: { @@ -125,7 +137,7 @@ export function getServiceMapBackendNodeInfo({ timeseries: response.aggregations?.timeseries.buckets.map( (bucket) => { return { - x: bucket.key, + x: bucket.key + offsetInMs, y: calculateThroughputWithRange({ start, end, @@ -139,7 +151,7 @@ export function getServiceMapBackendNodeInfo({ value: latency, timeseries: response.aggregations?.timeseries.buckets.map( (bucket) => ({ - x: bucket.key, + x: bucket.key + offsetInMs, y: bucket.latency_sum.value, }) ), diff --git a/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts index 545fb4dbc4606..ec6c13de76fb1 100644 --- a/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts @@ -22,6 +22,7 @@ import { TRANSACTION_REQUEST, } from '../../../common/transaction_types'; import { environmentQuery } from '../../../common/utils/environment_query'; +import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; import { getBucketSizeForAggregatedTransactions } from '../../lib/helpers/get_bucket_size_for_aggregated_transactions'; import { Setup } from '../../lib/helpers/setup_request'; import { @@ -43,6 +44,7 @@ interface Options { searchAggregatedTransactions: boolean; start: number; end: number; + offset?: string; } interface TaskParameters { @@ -57,6 +59,7 @@ interface TaskParameters { intervalString: string; bucketSize: number; numBuckets: number; + offsetInMs: number; } export function getServiceMapServiceNodeInfo({ @@ -66,11 +69,18 @@ export function getServiceMapServiceNodeInfo({ searchAggregatedTransactions, start, end, + offset, }: Options): Promise { return withApmSpan('get_service_map_node_stats', async () => { + const { offsetInMs, startWithOffset, endWithOffset } = getOffsetInMs({ + start, + end, + offset, + }); + const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, - ...rangeQuery(start, end), + ...rangeQuery(startWithOffset, endWithOffset), ...environmentQuery(environment), ]; @@ -90,11 +100,12 @@ export function getServiceMapServiceNodeInfo({ minutes, serviceName, setup, - start, - end, + start: startWithOffset, + end: endWithOffset, intervalString, bucketSize, numBuckets, + offsetInMs, }; const [failedTransactionsRate, transactionStats, cpuUsage, memoryUsage] = @@ -121,6 +132,7 @@ async function getFailedTransactionsRateStats({ start, end, numBuckets, + offsetInMs, }: TaskParameters): Promise { return withApmSpan('get_error_rate_for_service_map_node', async () => { const { average, timeseries } = await getFailedTransactionRate({ @@ -133,7 +145,10 @@ async function getFailedTransactionsRateStats({ kuery: '', numBuckets, }); - return { value: average, timeseries }; + return { + value: average, + timeseries: timeseries.map(({ x, y }) => ({ x: x + offsetInMs, y })), + }; }); } @@ -145,6 +160,7 @@ async function getTransactionStats({ start, end, intervalString, + offsetInMs, }: TaskParameters): Promise { const { apmEventClient } = setup; @@ -204,7 +220,7 @@ async function getTransactionStats({ latency: { value: response.aggregations?.duration.value ?? null, timeseries: response.aggregations?.timeseries.buckets.map((bucket) => ({ - x: bucket.key, + x: bucket.key + offsetInMs, y: bucket.latency.value, })), }, @@ -212,7 +228,7 @@ async function getTransactionStats({ value: totalRequests > 0 ? totalRequests / minutes : null, timeseries: response.aggregations?.timeseries.buckets.map((bucket) => { return { - x: bucket.key, + x: bucket.key + offsetInMs, y: bucket.doc_count ?? 0, }; }), @@ -226,6 +242,7 @@ async function getCpuStats({ intervalString, start, end, + offsetInMs, }: TaskParameters): Promise { const { apmEventClient } = setup; @@ -266,7 +283,7 @@ async function getCpuStats({ return { value: response.aggregations?.avgCpuUsage.value ?? null, timeseries: response.aggregations?.timeseries.buckets.map((bucket) => ({ - x: bucket.key, + x: bucket.key + offsetInMs, y: bucket.cpuAvg.value, })), }; @@ -278,6 +295,7 @@ function getMemoryStats({ intervalString, start, end, + offsetInMs, }: TaskParameters) { return withApmSpan('get_memory_stats_for_service_map_node', async () => { const { apmEventClient } = setup; @@ -324,7 +342,7 @@ function getMemoryStats({ return { value: response.aggregations?.avgMemoryUsage.value ?? null, timeseries: response.aggregations?.timeseries.buckets.map((bucket) => ({ - x: bucket.key, + x: bucket.key + offsetInMs, y: bucket.memoryAvg.value, })), }; diff --git a/x-pack/plugins/apm/server/routes/service_map/route.ts b/x-pack/plugins/apm/server/routes/service_map/route.ts index 97d0c01ed6a44..6b002e913204b 100644 --- a/x-pack/plugins/apm/server/routes/service_map/route.ts +++ b/x-pack/plugins/apm/server/routes/service_map/route.ts @@ -17,7 +17,7 @@ import { getServiceMapBackendNodeInfo } from './get_service_map_backend_node_inf import { getServiceMapServiceNodeInfo } from './get_service_map_service_node_info'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository'; -import { environmentRt, rangeRt } from '../default_api_types'; +import { environmentRt, offsetRt, rangeRt } from '../default_api_types'; const serviceMapRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/service-map', @@ -75,7 +75,7 @@ const serviceMapServiceNodeRoute = createApmServerRoute({ path: t.type({ serviceName: t.string, }), - query: t.intersection([environmentRt, rangeRt]), + query: t.intersection([environmentRt, rangeRt, offsetRt]), }), options: { tags: ['access:apm'] }, handler: async (resources) => { @@ -91,7 +91,7 @@ const serviceMapServiceNodeRoute = createApmServerRoute({ const { path: { serviceName }, - query: { environment, start, end }, + query: { environment, start, end, offset }, } = params; const searchAggregatedTransactions = await getSearchAggregatedTransactions({ @@ -102,14 +102,23 @@ const serviceMapServiceNodeRoute = createApmServerRoute({ kuery: '', }); - return getServiceMapServiceNodeInfo({ + const commonProps = { environment, setup, serviceName, searchAggregatedTransactions, start, end, - }); + }; + + const [currentPeriod, previousPeriod] = await Promise.all([ + getServiceMapServiceNodeInfo(commonProps), + offset + ? getServiceMapServiceNodeInfo({ ...commonProps, offset }) + : undefined, + ]); + + return { currentPeriod, previousPeriod }; }, }); @@ -120,6 +129,7 @@ const serviceMapBackendNodeRoute = createApmServerRoute({ t.type({ backendName: t.string }), environmentRt, rangeRt, + offsetRt, ]), }), options: { tags: ['access:apm'] }, @@ -135,16 +145,19 @@ const serviceMapBackendNodeRoute = createApmServerRoute({ const setup = await setupRequest(resources); const { - query: { backendName, environment, start, end }, + query: { backendName, environment, start, end, offset }, } = params; - return getServiceMapBackendNodeInfo({ - environment, - setup, - backendName, - start, - end, - }); + const commonProps = { environment, setup, backendName, start, end }; + + const [currentPeriod, previousPeriod] = await Promise.all([ + getServiceMapBackendNodeInfo(commonProps), + offset + ? getServiceMapBackendNodeInfo({ ...commonProps, offset }) + : undefined, + ]); + + return { currentPeriod, previousPeriod }; }, }); diff --git a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.spec.ts b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.spec.ts index d4f0e350071bf..454b129464c00 100644 --- a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.spec.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { isEmpty, orderBy, uniq } from 'lodash'; +import { first, isEmpty, last, orderBy, uniq } from 'lodash'; import { ServiceConnectionNode } from '../../../../plugins/apm/common/service_map'; import { ApmApiError, SupertestReturnType } from '../../common/apm_api_supertest'; import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; @@ -90,11 +90,11 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) it('returns an object with nulls', async () => { [ - response.body.failedTransactionsRate?.value, - response.body.memoryUsage?.value, - response.body.cpuUsage?.value, - response.body.transactionStats?.latency?.value, - response.body.transactionStats?.throughput?.value, + response.body.currentPeriod?.failedTransactionsRate?.value, + response.body.currentPeriod?.memoryUsage?.value, + response.body.currentPeriod?.cpuUsage?.value, + response.body.currentPeriod?.transactionStats?.latency?.value, + response.body.currentPeriod?.transactionStats?.throughput?.value, ].forEach((value) => { expect(value).to.be.eql(null); }); @@ -122,7 +122,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) }); it('returns undefined values', () => { - expect(response.body).to.eql({ transactionStats: {} }); + expect(response.body.currentPeriod).to.eql({ transactionStats: {} }); }); }); }); @@ -343,23 +343,31 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) }); it('returns some error rate', () => { - expect(response.body.failedTransactionsRate?.value).to.eql(0); - expect(response.body.failedTransactionsRate?.timeseries?.length).to.be.greaterThan(0); + expect(response.body.currentPeriod?.failedTransactionsRate?.value).to.eql(0); + expect( + response.body.currentPeriod?.failedTransactionsRate?.timeseries?.length + ).to.be.greaterThan(0); }); it('returns some latency', () => { - expect(response.body.transactionStats?.latency?.value).to.be.greaterThan(0); - expect(response.body.transactionStats?.latency?.timeseries?.length).to.be.greaterThan(0); + expect(response.body.currentPeriod?.transactionStats?.latency?.value).to.be.greaterThan(0); + expect( + response.body.currentPeriod?.transactionStats?.latency?.timeseries?.length + ).to.be.greaterThan(0); }); it('returns some throughput', () => { - expect(response.body.transactionStats?.throughput?.value).to.be.greaterThan(0); - expect(response.body.transactionStats?.throughput?.timeseries?.length).to.be.greaterThan(0); + expect(response.body.currentPeriod?.transactionStats?.throughput?.value).to.be.greaterThan( + 0 + ); + expect( + response.body.currentPeriod?.transactionStats?.throughput?.timeseries?.length + ).to.be.greaterThan(0); }); it('returns some cpu usage', () => { - expect(response.body.cpuUsage?.value).to.be.greaterThan(0); - expect(response.body.cpuUsage?.timeseries?.length).to.be.greaterThan(0); + expect(response.body.currentPeriod?.cpuUsage?.value).to.be.greaterThan(0); + expect(response.body.currentPeriod?.cpuUsage?.timeseries?.length).to.be.greaterThan(0); }); }); @@ -384,18 +392,134 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) }); it('returns some error rate', () => { - expect(response.body.failedTransactionsRate?.value).to.eql(0); - expect(response.body.failedTransactionsRate?.timeseries?.length).to.be.greaterThan(0); + expect(response.body.currentPeriod?.failedTransactionsRate?.value).to.eql(0); + expect( + response.body.currentPeriod?.failedTransactionsRate?.timeseries?.length + ).to.be.greaterThan(0); }); it('returns some latency', () => { - expect(response.body.transactionStats?.latency?.value).to.be.greaterThan(0); - expect(response.body.transactionStats?.latency?.timeseries?.length).to.be.greaterThan(0); + expect(response.body.currentPeriod?.transactionStats?.latency?.value).to.be.greaterThan(0); + expect( + response.body.currentPeriod?.transactionStats?.latency?.timeseries?.length + ).to.be.greaterThan(0); }); it('returns some throughput', () => { - expect(response.body.transactionStats?.throughput?.value).to.be.greaterThan(0); - expect(response.body.transactionStats?.throughput?.timeseries?.length).to.be.greaterThan(0); + expect(response.body.currentPeriod?.transactionStats?.throughput?.value).to.be.greaterThan( + 0 + ); + expect( + response.body.currentPeriod?.transactionStats?.throughput?.timeseries?.length + ).to.be.greaterThan(0); + }); + }); + + describe('With comparison', () => { + describe('/internal/apm/service-map/backend', () => { + let response: BackendResponse; + before(async () => { + response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/service-map/backend`, + params: { + query: { + backendName: 'postgresql', + start: metadata.start, + end: metadata.end, + environment: 'ENVIRONMENT_ALL', + offset: '5m', + }, + }, + }); + }); + + it('returns some data', () => { + const { currentPeriod, previousPeriod } = response.body; + [ + currentPeriod.failedTransactionsRate, + previousPeriod?.failedTransactionsRate, + currentPeriod.transactionStats?.latency, + previousPeriod?.transactionStats?.latency, + currentPeriod.transactionStats?.throughput, + previousPeriod?.transactionStats?.throughput, + ].map((value) => expect(value?.timeseries?.length).to.be.greaterThan(0)); + }); + + it('has same start time for both periods', () => { + const { currentPeriod, previousPeriod } = response.body; + expect(first(currentPeriod.failedTransactionsRate?.timeseries)?.x).to.equal( + first(previousPeriod?.failedTransactionsRate?.timeseries)?.x + ); + }); + + it('has same end time for both periods', () => { + const { currentPeriod, previousPeriod } = response.body; + expect(last(currentPeriod.failedTransactionsRate?.timeseries)?.x).to.equal( + last(previousPeriod?.failedTransactionsRate?.timeseries)?.x + ); + }); + + it('returns same number of buckets for both periods', () => { + const { currentPeriod, previousPeriod } = response.body; + expect(currentPeriod.failedTransactionsRate?.timeseries?.length).to.be( + previousPeriod?.failedTransactionsRate?.timeseries?.length + ); + }); + }); + + describe('/internal/apm/service-map/service/{serviceName}', () => { + let response: ServiceNodeResponse; + before(async () => { + response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/service-map/service/{serviceName}`, + params: { + path: { serviceName: 'opbeans-node' }, + query: { + start: metadata.start, + end: metadata.end, + environment: 'ENVIRONMENT_ALL', + offset: '5m', + }, + }, + }); + }); + + it('returns some data', () => { + const { currentPeriod, previousPeriod } = response.body; + [ + currentPeriod.failedTransactionsRate, + previousPeriod?.failedTransactionsRate, + currentPeriod.transactionStats?.latency, + previousPeriod?.transactionStats?.latency, + currentPeriod.transactionStats?.throughput, + previousPeriod?.transactionStats?.throughput, + currentPeriod.cpuUsage, + previousPeriod?.cpuUsage, + currentPeriod.memoryUsage, + previousPeriod?.memoryUsage, + ].map((value) => expect(value?.timeseries?.length).to.be.greaterThan(0)); + }); + + it('has same start time for both periods', () => { + const { currentPeriod, previousPeriod } = response.body; + expect(first(currentPeriod.failedTransactionsRate?.timeseries)?.x).to.equal( + first(previousPeriod?.failedTransactionsRate?.timeseries)?.x + ); + }); + + it('has same end time for both periods', () => { + const { currentPeriod, previousPeriod } = response.body; + expect(last(currentPeriod.failedTransactionsRate?.timeseries)?.x).to.equal( + last(previousPeriod?.failedTransactionsRate?.timeseries)?.x + ); + }); + + it('returns same number of buckets for both periods', () => { + const { currentPeriod, previousPeriod } = response.body; + expect(currentPeriod.failedTransactionsRate?.timeseries?.length).to.be( + previousPeriod?.failedTransactionsRate?.timeseries?.length + ); + }); }); }); }); From 6c4c5e12995caa56d66add3f40741ae640ea3445 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Mon, 13 Dec 2021 11:47:03 -0500 Subject: [PATCH 042/145] Some updates to our dev docs (#120981) * Master -> main update branching strategy with make it minor * Follow the new dev docs process and keep nav inside this repo * add back nav links that are in a different repo Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- dev_docs/api_welcome.mdx | 2 +- dev_docs/contributing/best_practices.mdx | 9 +- dev_docs/contributing/code_walkthrough.mdx | 2 +- dev_docs/contributing/how_we_use_github.mdx | 26 ++- dev_docs/tutorials/submit_a_pull_request.mdx | 4 +- nav-kibana-dev.docnav.json | 160 ++++++++++++++++++ src/dev/precommit_hook/casing_check_config.js | 3 + 7 files changed, 186 insertions(+), 20 deletions(-) create mode 100644 nav-kibana-dev.docnav.json diff --git a/dev_docs/api_welcome.mdx b/dev_docs/api_welcome.mdx index cca911cc6cdd0..cf88bf7eec0da 100644 --- a/dev_docs/api_welcome.mdx +++ b/dev_docs/api_welcome.mdx @@ -44,7 +44,7 @@ This documentation is being automatically generated using an There is one extra step required to have your API docs show up in the _navigation_ of the docs system. Follow the instructions to learn how to configure the navigation menu. The nav file you need to - edit is: [https://github.com/elastic/elastic-docs/blob/master/config/nav-kibana-dev.ts](https://github.com/elastic/elastic-docs/blob/master/config/nav-kibana-dev.ts) + edit is: [https://github.com/elastic/elastic-docs/blob/main/config/nav-kibana-dev.ts](https://github.com/elastic/elastic-docs/blob/main/config/nav-kibana-dev.ts) Your API docs will exist in the top level [`api_docs` folder](https://github.com/elastic/kibana/tree/main/api_docs) and will use a doc id of the pattern `kib${PluginName}PluginApi`. diff --git a/dev_docs/contributing/best_practices.mdx b/dev_docs/contributing/best_practices.mdx index d0ae34155d7eb..d7aa42946eac3 100644 --- a/dev_docs/contributing/best_practices.mdx +++ b/dev_docs/contributing/best_practices.mdx @@ -141,6 +141,9 @@ export type foo: string | AnInterface; Running Kibana with `yarn start --run-examples` will include all [example plugins](https://github.com/elastic/kibana/tree/main/examples). These are tested examples of platform services in use. We strongly encourage anyone providing a platform level service or to include a tutorial that links to a tested example plugin. This is better than relying on copied code snippets, which can quickly get out of date. +You can also visit these [examples plugins hosted online](https://demo.kibana.dev/8.0/app/home). Note that because anonymous access is enabled, some +of the demos are currently not working. + ## Performance Build with scalability in mind. @@ -150,6 +153,8 @@ Build with scalability in mind. - Consider large data sets, that span a long time range - Consider slow internet and low bandwidth environments + + ## Accessibility Did you know Kibana makes a public statement about our commitment to creating an accessible product for people @@ -202,13 +207,13 @@ Kibana code base, try not to contribute to this volatility. Doing this can: All of the above contributes to more bugs being found in the QA cycle and can cause a delay in the release. Prefer instead to merge your large change right _after_ feature freeze. If you are worried about missing your initial release version goals, review our -[release train philophy](https://github.com/elastic/dev/blob/master/shared/time-based-releases.md). It's okay! +. It's okay! ### Size -When possible, build features with incrementals sets of small and focused PRs, but don't check in unused code, and don't expose any feature on master that you would not be comfortable releasing. +When possible, build features with incremental sets of small and focused PRs, but don't check in unused code, and don't expose any feature on main that you would not be comfortable releasing. ![product_stages](../assets/product_stages.png) diff --git a/dev_docs/contributing/code_walkthrough.mdx b/dev_docs/contributing/code_walkthrough.mdx index 62965add07578..74995c246503c 100644 --- a/dev_docs/contributing/code_walkthrough.mdx +++ b/dev_docs/contributing/code_walkthrough.mdx @@ -21,7 +21,7 @@ Managed by the operations team to contain Jenkins settings. Can be ignored by fo ## [.github](https://github.com/elastic/kibana/tree/main/.github) -Contains GitHub configuration settings. This file contains issue templates, and the [CODEOWNERS](https://github.com/elastic/kibana/blob/main/.github/CODEOWNERS) file. It's important for teams to keep the CODEOWNERS file up-to-date so the right team is pinged for a code owner review on PRs that edit certain files. Note that the `CODEOWNERS` file only exists on the main/master branch, and is not backported to other branches in the repo. +Contains GitHub configuration settings. This file contains issue templates, and the [CODEOWNERS](https://github.com/elastic/kibana/blob/main/.github/CODEOWNERS) file. It's important for teams to keep the CODEOWNERS file up-to-date so the right team is pinged for a code owner review on PRs that edit certain files. Note that the `CODEOWNERS` file only exists on the main branch, and is not backported to other branches in the repo. ## [api_docs](https://github.com/elastic/kibana/tree/main/api_docs) diff --git a/dev_docs/contributing/how_we_use_github.mdx b/dev_docs/contributing/how_we_use_github.mdx index 38391874b87bf..247fca97335c7 100644 --- a/dev_docs/contributing/how_we_use_github.mdx +++ b/dev_docs/contributing/how_we_use_github.mdx @@ -15,16 +15,14 @@ We follow the [GitHub forking model](https://help.github.com/articles/fork-a-rep At Elastic, all products in the stack, including Kibana, are released at the same time with the same version number. Most of these projects have the following branching strategy: -- master is the next major version. -- `.x` is the next minor version. -- `.` is the next release of a minor version, including patch releases. +- `main` points to the next minor version. +- `.` is the previously released minor version, including patch releases. -As an example, let’s assume that the 7.x branch is currently a not-yet-released 7.6.0. Once 7.6.0 has reached feature freeze, it will be branched to 7.6 and 7.x will be updated to reflect 7.7.0. The release of 7.6.0 and subsequent patch releases will be cut from the 7.6 branch. At any time, you can verify the current version of a branch by inspecting the version attribute in the package.json file within the Kibana source. +As an example, let’s assume that the main branch is currently a not-yet-released 8.1.0. Once 8.1.0 has reached feature freeze, it will be branched to 8.1 and main will be updated to reflect 8.2.0. The release of 8.1.0 and subsequent patch releases will be cut from the 8.1 branch. At any time, you can verify the current version of a branch by inspecting the version attribute in the package.json file within the Kibana source. -Pull requests are made into the master branch and then backported when it is safe and appropriate. +Pull requests are made into the main branch and only backported when it is safe and appropriate. -- Breaking changes do not get backported and only go into master. -- All non-breaking changes can be backported to the `.x` branch. +- Breaking changes can _only_ be made to `main` if there has been at least an 18 month deprecation period _and_ the breaking change has been approved. Telemetry showing current usage is crucial for gaining approval. - Features should not be backported to a `.` branch. - Bug fixes can be backported to a `.` branch if the changes are safe and appropriate. Safety is a judgment call you make based on factors like the bug’s severity, test coverage, confidence in the changes, etc. Your reasoning should be included in the pull request description. - Documentation changes can be backported to any branch at any time. @@ -63,26 +61,26 @@ In order to assist with developer tooling we ask that all Elastic engineers use Rebasing can be tricky, and fixing merge conflicts can be even trickier because it involves force pushing. This is all compounded by the fact that attempting to push a rebased branch remotely will be rejected by git, and you’ll be prompted to do a pull, which is not at all what you should do (this will really mess up your branch’s history). -Here’s how you should rebase master onto your branch, and how to fix merge conflicts when they arise. +Here’s how you should rebase main onto your branch, and how to fix merge conflicts when they arise. -First, make sure master is up-to-date. +First, make sure main is up-to-date. ```bash -git checkout master +git checkout main git fetch upstream -git rebase upstream/master +git rebase upstream/main ``` -Then, check out your branch and rebase master on top of it, which will apply all of the new commits on master to your branch, and then apply all of your branch’s new commits after that. +Then, check out your branch and rebase main on top of it, which will apply all of the new commits on main to your branch, and then apply all of your branch’s new commits after that. ```bash git checkout name-of-your-branch -git rebase master +git rebase main ``` You want to make sure there are no merge conflicts. If there are merge conflicts, git will pause the rebase and allow you to fix the conflicts before continuing. -You can use git status to see which files contain conflicts. They’ll be the ones that aren’t staged for commit. Open those files, and look for where git has marked the conflicts. Resolve the conflicts so that the changes you want to make to the code have been incorporated in a way that doesn’t destroy work that’s been done in master. Refer to master’s commit history on GitHub if you need to gain a better understanding of how code is conflicting and how best to resolve it. +You can use git status to see which files contain conflicts. They’ll be the ones that aren’t staged for commit. Open those files, and look for where git has marked the conflicts. Resolve the conflicts so that the changes you want to make to the code have been incorporated in a way that doesn’t destroy work that’s been done in main. Refer to main commit history on GitHub if you need to gain a better understanding of how code is conflicting and how best to resolve it. Once you’ve resolved all of the merge conflicts, use git add -A to stage them to be committed, and then use git rebase --continue to tell git to continue the rebase. diff --git a/dev_docs/tutorials/submit_a_pull_request.mdx b/dev_docs/tutorials/submit_a_pull_request.mdx index 5436ebf24e03e..f7d530f6cec66 100644 --- a/dev_docs/tutorials/submit_a_pull_request.mdx +++ b/dev_docs/tutorials/submit_a_pull_request.mdx @@ -23,7 +23,7 @@ After cloning your fork and navigating to the directory containing your fork: ```bash # Make sure you currently have the branch checked out off of which you'd like to work -git checkout master +git checkout main # Create a new branch git checkout -b fix-typos-in-readme @@ -76,7 +76,7 @@ See [Pull request review guidelines](https://www.elastic.co/guide/en/kibana/mast ## Updating your PR with upstream -If your pull request hasn't been updated with the latest code from the upstream/target branch, e.g. `master`, in the last 48 hours, it won't be able to merge until it is updated. This is to help prevent problems that could occur by merging stale code into upstream, e.g. something new was recently merged that is incompatible with something in your pull request. +If your pull request hasn't been updated with the latest code from the upstream/target branch, e.g. `main`, in the last 48 hours, it won't be able to merge until it is updated. This is to help prevent problems that could occur by merging stale code into upstream, e.g. something new was recently merged that is incompatible with something in your pull request. As an alternative to using `git` to manually update your branch, you can leave a comment on your pull request with the text `@elasticmachine merge upstream`. This will automatically update your branch and kick off CI for it. diff --git a/nav-kibana-dev.docnav.json b/nav-kibana-dev.docnav.json new file mode 100644 index 0000000000000..384652f373358 --- /dev/null +++ b/nav-kibana-dev.docnav.json @@ -0,0 +1,160 @@ +{ + "mission": "Kibana Developer Guide", + "id": "kibDevDocs", + "landingPageId": "kibDevDocsWelcome", + "icon": "logoKibana", + "description": "Developer documentation for building custom Kibana plugins and extending Kibana functionality.", + "items": [ + { + "category": "Getting started", + "items": [ + { "id": "kibDevDocsWelcome" }, + { "id": "kibDevTutorialSetupDevEnv" }, + { "id": "kibHelloWorldApp" }, + { "id": "kibDevAddData" }, + { "id": "kibTroubleshooting" } + ] + }, + { + "category": "Key concepts", + "items": [ + { "id": "kibPlatformIntro" }, + { "id": "kibDevAnatomyOfAPlugin" }, + { "id": "kibDevPerformance" }, + { "id": "kibBuildingBlocks" }, + { "id": "kibDevDocsSavedObjectsIntro", "label": "Saved objects" }, + { "id": "kibDevDocsPersistableStateIntro" }, + { "id": "kibDataPlugin", "label": "Data" }, + { "id": "kibCoreLogging" }, + { "id": "kibUsageCollectionPlugin" }, + { "id": "kibDataViewsKeyConcepts" }, + { "id": "kibDevKeyConceptsNavigation" } + ] + }, + { + "category": "Tutorials", + "items": [ + { "id": "kibDevTutorialTestingPlugins" }, + { "id": "kibDevTutorialSavedObject" }, + { "id": "kibDevTutorialSubmitPullRequest" }, + { "id": "kibDevTutorialExpressions" }, + { "id": "kibDevDocsKPTTutorial" }, + { "id": "kibDevTutorialDataSearchAndSessions", "label": "data.search" }, + { "id": "kibDevTutorialDataViews" }, + { "id": "kibDevTutorialDebugging" }, + { + "id": "kibDevTutorialBuildingDistributable", + "label": "Building a Kibana distributable" + }, + { "id": "kibDevTutorialServerEndpoint" } + ] + }, + { + "category": "Contributing", + "items": [ + { "id": "kibRepoStructure" }, + { "id": "kibDevPrinciples" }, + { "id": "kibStandards" }, + { "id": "ktRFCProcess" }, + { "id": "kibBestPractices" }, + { "id": "kibStyleGuide" }, + { "id": "kibGitHub" } + ] + }, + { + "category": "Contributors Newsletters", + "items": [ + { "id": "kibNovember2021ContributorNewsletter" }, + { "id": "kibOctober2021ContributorNewsletter" }, + { "id": "kibSeptember2021ContributorNewsletter" }, + { "id": "kibAugust2021ContributorNewsletter" }, + { "id": "kibJuly2021ContributorNewsletter" }, + { "id": "kibJune2021ContributorNewsletter" }, + { "id": "kibMay2021ContributorNewsletter" }, + { "id": "kibApril2021ContributorNewsletter" }, + { "id": "kibMarch2021ContributorNewsletter" } + ] + }, + { + "category": "API documentation", + "items": [ + { "id": "kibDevDocsApiWelcome" }, + { "id": "kibDevDocsPluginDirectory" }, + { "id": "kibDevDocsDeprecationsByPlugin" }, + { "id": "kibDevDocsDeprecationsByApi" }, + { "id": "kibCorePluginApi" }, + { "id": "kibCoreApplicationPluginApi" }, + { "id": "kibCoreChromePluginApi" }, + { "id": "kibCoreHttpPluginApi" }, + { "id": "kibCoreSavedObjectsPluginApi" }, + { "id": "kibFieldFormatsPluginApi" }, + { "id": "kibDataPluginApi" }, + { "id": "kibDataAutocompletePluginApi" }, + { "id": "kibDataEnhancedPluginApi" }, + { "id": "kibDataViewsPluginApi" }, + { "id": "kibDataQueryPluginApi" }, + { "id": "kibDataSearchPluginApi" }, + { "id": "kibDataUiPluginApi" }, + { "id": "kibBfetchPluginApi" }, + { "id": "kibAlertingPluginApi" }, + { "id": "kibTaskManagerPluginApi" }, + { "id": "kibActionsPluginApi" }, + { "id": "kibEventLogPluginApi" }, + { "id": "kibTriggersActionsUiPluginApi" }, + { "id": "kibCasesPluginApi" }, + { "id": "kibChartsPluginApi" }, + { "id": "kibDashboardPluginApi" }, + { "id": "kibDevToolsPluginApi" }, + { "id": "kibDiscoverPluginApi" }, + { "id": "kibEmbeddablePluginApi" }, + { "id": "kibEncryptedSavedObjectsPluginApi" }, + { "id": "kibEnterpriseSearchPluginApi" }, + { "id": "kibEsUiSharedPluginApi" }, + { "id": "kibExpressionsPluginApi" }, + { "id": "kibFeaturesPluginApi" }, + { "id": "kibFileUploadPluginApi" }, + { "id": "kibFleetPluginApi" }, + { "id": "kibGlobalSearchPluginApi" }, + { "id": "kibHomePluginApi" }, + { "id": "kibInspectorPluginApi" }, + { "id": "kibKibanaReactPluginApi" }, + { "id": "kibKibanaUtilsPluginApi" }, + { "id": "kibLensPluginApi" }, + { "id": "kibLicenseManagementPluginApi" }, + { "id": "kibLicensingPluginApi" }, + { "id": "kibListsPluginApi" }, + { "id": "kibManagementPluginApi" }, + { "id": "kibMapsPluginApi" }, + { "id": "kibMlPluginApi" }, + { "id": "kibMonitoringPluginApi" }, + { "id": "kibNavigationPluginApi" }, + { "id": "kibNewsfeedPluginApi" }, + { "id": "kibObservabilityPluginApi" }, + { "id": "kibRemoteClustersPluginApi" }, + { "id": "kibReportingPluginApi" }, + { "id": "kibRollupPluginApi" }, + { "id": "kibRuntimeFieldsPluginApi" }, + { "id": "kibSavedObjectsManagementPluginApi" }, + { "id": "kibSavedObjectsTaggingOssPluginApi" }, + { "id": "kibSavedObjectsTaggingPluginApi" }, + { "id": "kibSavedObjectsPluginApi" }, + { "id": "kibSecuritySolutionPluginApi" }, + { "id": "kibSecurityPluginApi" }, + { "id": "kibSharePluginApi" }, + { "id": "kibSnapshotRestorePluginApi" }, + { "id": "kibSpacesPluginApi" }, + { "id": "kibStackAlertsPluginApi" }, + { "id": "kibTelemetryCollectionManagerPluginApi" }, + { "id": "kibTelemetryCollectionXpackPluginApi" }, + { "id": "kibTelemetryManagementSectionPluginApi" }, + { "id": "kibTelemetryPluginApi" }, + { "id": "kibUiActionsEnhancedPluginApi" }, + { "id": "kibUiActionsPluginApi" }, + { "id": "kibUrlForwardingPluginApi" }, + { "id": "kibUsageCollectionPluginApi" }, + { "id": "kibVisTypeTimeseriesPluginApi" }, + { "id": "kibVisualizationsPluginApi" } + ] + } + ] +} diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index e3d9688e60962..e885180cdb803 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -42,6 +42,9 @@ export const IGNORE_FILE_GLOBS = [ 'test/package/Vagrantfile', '**/test/**/fixtures/**/*', + // Required to match the name in the docs.elastic.dev repo. + 'nav-kibana-dev.docnav.json', + // filename must match language code which requires capital letters '**/translations/*.json', From cc9be33dad0ae710019ebaac93292ea0095b602f Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Mon, 13 Dec 2021 11:56:40 -0500 Subject: [PATCH 043/145] [Security Solutio][Investigations] Update Timeline Details API with ECS field (#120683) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/types/timeline/index.ts | 2 - .../__snapshots__/index.test.tsx.snap | 2641 ---------------- .../side_panel/event_details/footer.tsx | 5 +- .../side_panel/event_details/index.tsx | 7 +- .../components/side_panel/index.test.tsx | 2685 ++++++++++++++++- .../timelines/containers/details/index.tsx | 13 +- .../timeline/events/details/index.ts | 2 + .../t_grid/body/events/stateful_event.tsx | 3 +- .../t_grid/body/row_action/index.tsx | 3 +- .../search_strategy/timeline/eql/helpers.ts | 4 +- .../timeline/factory/events/all/index.ts | 5 +- .../timeline/factory/events/details/index.ts | 4 +- .../factory/helpers/build_ecs_objects.test.ts | 134 + .../factory/helpers/build_ecs_objects.ts | 33 + .../helpers/build_fields_request.test.ts | 34 + .../factory/helpers/build_fields_request.ts | 18 + .../build_object_for_field_path.test.ts | 179 ++ .../helpers/build_object_for_field_path.ts | 37 + .../{events/all => helpers}/constants.ts | 3 +- .../format_timeline_data.test.ts} | 179 +- .../format_timeline_data.ts} | 76 +- .../helpers/get_nested_parent_path.test.ts | 39 + .../factory/helpers/get_nested_parent_path.ts | 18 + .../timeline/factory/helpers/get_timestamp.ts | 17 + 24 files changed, 3214 insertions(+), 2927 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/build_ecs_objects.test.ts create mode 100644 x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/build_ecs_objects.ts create mode 100644 x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/build_fields_request.test.ts create mode 100644 x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/build_fields_request.ts create mode 100644 x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/build_object_for_field_path.test.ts create mode 100644 x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/build_object_for_field_path.ts rename x-pack/plugins/timelines/server/search_strategy/timeline/factory/{events/all => helpers}/constants.ts (98%) rename x-pack/plugins/timelines/server/search_strategy/timeline/factory/{events/all/helpers.test.ts => helpers/format_timeline_data.test.ts} (72%) rename x-pack/plugins/timelines/server/search_strategy/timeline/factory/{events/all/helpers.ts => helpers/format_timeline_data.ts} (60%) create mode 100644 x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/get_nested_parent_path.test.ts create mode 100644 x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/get_nested_parent_path.ts create mode 100644 x-pack/plugins/timelines/server/search_strategy/timeline/factory/helpers/get_timestamp.ts diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 442986870ac94..ac8fb19e00df5 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -22,7 +22,6 @@ import { import { FlowTarget } from '../../search_strategy/security_solution/network'; import { errorSchema } from '../../detection_engine/schemas/response/error_schema'; import { Direction, Maybe } from '../../search_strategy'; -import { Ecs } from '../../ecs'; export * from './actions'; export * from './cells'; @@ -503,7 +502,6 @@ export type TimelineExpandedEventType = eventId: string; indexName: string; refetch?: () => void; - ecsData?: Ecs; }; } | EmptyObject; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 80d8e8f9b9e26..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,2641 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Details Panel Component DetailsPanel: rendering it should not render the DetailsPanel if an expanded detail with a panelView, but not params have been set 1`] = ` - -`; - -exports[`Details Panel Component DetailsPanel: rendering it should not render the DetailsPanel if no expanded detail has been set in the reducer 1`] = ` - -`; - -exports[`Details Panel Component DetailsPanel:EventDetails: rendering it should render the Details Panel when the panelView is set and the associated params are set 1`] = ` -.c0 { - -webkit-flex: 0 1 auto; - -ms-flex: 0 1 auto; - flex: 0 1 auto; - margin-top: 8px; -} - - - - - - -
- -
- - -
- - - -
-
-
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; - -exports[`Details Panel Component DetailsPanel:EventDetails: rendering it should render the Event Details view in the Details Panel when the panelView is eventDetail and the eventId is set 1`] = `null`; - -exports[`Details Panel Component DetailsPanel:EventDetails: rendering it should render the Event Details view of the Details Panel in the flyout when the panelView is eventDetail and the eventId is set 1`] = ` -Array [ - .c0 { - -webkit-flex: 0 1 auto; - -ms-flex: 0 1 auto; - flex: 0 1 auto; - margin-top: 8px; -} - -.c1 .euiFlyoutBody__overflow { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - overflow: hidden; -} - -.c1 .euiFlyoutBody__overflow .euiFlyoutBody__overflowContent { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - overflow: hidden; - padding: 0 16px 16px; -} - - -
- + +
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `); }); test('it should render the Event Details view of the Details Panel in the flyout when the panelView is eventDetail and the eventId is set', () => { @@ -156,17 +379,584 @@ describe('Details Panel Component', () => { ); - expect(wrapper.find('[data-test-subj="timeline:details-panel:flyout"]')).toMatchSnapshot(); - }); + expect(wrapper.find('[data-test-subj="timeline:details-panel:flyout"]')) + .toMatchInlineSnapshot(` + Array [ + .c0 { + -webkit-flex: 0 1 auto; + -ms-flex: 0 1 auto; + flex: 0 1 auto; + margin-top: 8px; + } - test('it should render the Event Details view in the Details Panel when the panelView is eventDetail and the eventId is set', () => { - const wrapper = mount( - - - - ); + .c1 .euiFlyoutBody__overflow { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + overflow: hidden; + } + + .c1 .euiFlyoutBody__overflow .euiFlyoutBody__overflowContent { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + overflow: hidden; + padding: 0 16px 16px; + } + + +
+