From d69e598e30977d7601147c2d40098388d98e1c2a Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 19 Aug 2024 11:31:16 +0200 Subject: [PATCH 01/25] [Infra] Handle view in app for legacy metrics (#190295) closes [#189625](https://github.com/elastic/kibana/issues/189625) ## Summary This PR changes the asset details to display a call if the user comes from the alerts page via an inventory rule created with one of the legacy metrics. Besides that, it changes how the link is built to use locators. Legacy metrics example https://github.com/user-attachments/assets/12308f4e-e269-4580-b86d-808ae9f6fe10 **Regression** Metrics Threshold https://github.com/user-attachments/assets/94032f51-6b2c-4760-8019-158746a1aa13 Inventory Rule (new/hosts view metrics) https://github.com/user-attachments/assets/0f872f3a-7bdb-4fb8-a925-7ed3621fee2d Inventory Rule (custom metric) https://github.com/user-attachments/assets/f2e5ded5-b2e6-45ff-878d-6361c4540140 ### Fix While working on it, I discovered that alerts for containers were not redirecting the users to the asset details page for containers. That was fixed too Inventory rule for containers https://github.com/user-attachments/assets/05f20c12-6fdc-45c0-bc38-b756bfbf3658 Metrics threshold rule for containers ### How to test - Start a local Kibana instance (easier if pointed to an oblt cluster) - Create Inventory Rule alerts for: - host: 1 legacy metric and 1 non-legacy metric - container - Create Metric Threshold alerts with - avg on `system.cpu.total.norm.pct` grouped by `host.name` - avg on `kubernetes.container.cpu.usage.limit.pct` grouped by `container.id` - Navigate to the alerts page and click on the `view in app` button, as shown in the recordings above - Test if the navigation to the asset details page works - For a legacy metric, the callout should be displayed - Once dismissed, the callout should not appear again for that metric --- .../alerting/metrics/alert_link.test.ts | 143 ++++++++++-- .../common/alerting/metrics/alert_link.ts | 206 ++++++++++++------ .../infra/common/asset_details/types.ts | 10 + .../infra/common/constants.ts | 1 - .../infra/public/alerting/inventory/index.ts | 19 +- .../inventory/rule_data_formatters.ts | 24 +- .../public/alerting/metric_threshold/index.ts | 12 +- .../metric_threshold/rule_data_formatters.ts | 31 ++- .../components/asset_details/constants.ts | 13 +- .../asset_details/content/callouts.tsx | 48 ++++ .../callouts/legacy_metric_callout.tsx | 82 +++++++ .../asset_details/content/content.tsx | 30 ++- .../hooks/use_asset_details_url_state.ts | 1 + .../asset_details/hooks/use_page_header.tsx | 4 +- .../asset_details/template/page.tsx | 26 +-- .../public/components/asset_details/types.ts | 7 +- .../redirect_to_host_detail_via_ip.tsx | 7 +- .../pages/link_to/redirect_to_node_detail.tsx | 9 +- .../infra/public/plugin.ts | 15 +- .../infra/server/lib/alerting/common/utils.ts | 31 ++- ...nventory_metric_threshold_executor.test.ts | 6 + .../inventory_metric_threshold_executor.ts | 32 ++- .../metric_threshold_executor.test.ts | 6 + .../metric_threshold_executor.ts | 36 ++- .../lib/helpers/get_apm_data_access_client.ts | 6 +- .../infra/server/lib/infra_types.ts | 16 +- .../infra/server/plugin.ts | 18 +- .../infra/server/routes/services/index.ts | 2 +- .../metrics_data_access/common/index.ts | 1 + .../common/inventory_models/host/index.ts | 1 + .../common/inventory_models/index.ts | 2 +- .../common/inventory_models/types.ts | 1 + .../formatters/snapshot_metric_formats.ts | 6 + .../locators/infra/asset_details_locator.ts | 23 +- .../locators/infra/inventory_locator.ts | 6 +- .../common/locators/infra/locators.test.ts | 4 +- .../observability_shared/public/plugin.ts | 1 + .../functional/apps/infra/node_details.ts | 96 ++++++-- .../functional/page_objects/asset_details.ts | 5 + 39 files changed, 770 insertions(+), 217 deletions(-) create mode 100644 x-pack/plugins/observability_solution/infra/common/asset_details/types.ts create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts.tsx create mode 100644 x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts/legacy_metric_callout.tsx diff --git a/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.test.ts b/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.test.ts index 441922d3fb77..2513a3432742 100644 --- a/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.test.ts +++ b/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.test.ts @@ -7,13 +7,43 @@ import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields'; import { ALERT_RULE_PARAMETERS, TIMESTAMP } from '@kbn/rule-data-utils'; +import rison from '@kbn/rison'; import { getInventoryViewInAppUrl, flatAlertRuleParams, getMetricsViewInAppUrl, } from './alert_link'; +import { + InventoryLocator, + AssetDetailsLocator, + InventoryLocatorParams, + AssetDetailsLocatorParams, +} from '@kbn/observability-shared-plugin/common'; + +jest.mock('@kbn/observability-shared-plugin/common'); + +const mockInventoryLocator = { + getRedirectUrl: jest + .fn() + .mockImplementation( + (params: InventoryLocatorParams) => + `/inventory-mock?receivedParams=${rison.encodeUnknown(params)}` + ), +} as unknown as jest.Mocked; + +const mockAssetDetailsLocator = { + getRedirectUrl: jest + .fn() + .mockImplementation( + ({ assetId, assetType, assetDetails }: AssetDetailsLocatorParams) => + `/node-mock/${assetType}/${assetId}?receivedParams=${rison.encodeUnknown(assetDetails)}` + ), +} as unknown as jest.Mocked; describe('Inventory Threshold Rule', () => { + afterEach(() => { + jest.clearAllMocks(); + }); describe('flatAlertRuleParams', () => { it('flat ALERT_RULE_PARAMETERS', () => { expect( @@ -85,9 +115,14 @@ describe('Inventory Threshold Rule', () => { [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`]: ['avg'], [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.field`]: ['system.cpu.user.pct'], } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockInventoryLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/inventory?customMetric=%28aggregation%3Aavg%2Cfield%3Asystem.cpu.user.pct%2Cid%3Aalert-custom-metric%2Ctype%3Acustom%29&metric=%28aggregation%3Aavg%2Cfield%3Asystem.cpu.user.pct%2Cid%3Aalert-custom-metric%2Ctype%3Acustom%29&nodeType=h×tamp=1640995200000' + "/inventory-mock?receivedParams=(customMetric:'(aggregation:avg,field:system.cpu.user.pct,id:alert-custom-metric,type:custom)',metric:'(aggregation:avg,field:system.cpu.user.pct,id:alert-custom-metric,type:custom)',nodeType:host,timestamp:1640995200000)" ); }); it('should work with non-custom metrics', () => { @@ -96,22 +131,50 @@ describe('Inventory Threshold Rule', () => { [`${ALERT_RULE_PARAMETERS}.nodeType`]: 'host', [`${ALERT_RULE_PARAMETERS}.criteria.metric`]: ['cpu'], } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockInventoryLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/inventory?customMetric=&metric=%28type%3Acpu%29&nodeType=h×tamp=1640995200000' + "/inventory-mock?receivedParams=(customMetric:'',metric:'(type:cpu)',nodeType:host,timestamp:1640995200000)" ); }); - it('should point to host-details when host.name is present', () => { + it('should point to asset details when nodeType is host and host.name is present', () => { const fields = { [TIMESTAMP]: '2022-01-01T00:00:00.000Z', - [`${ALERT_RULE_PARAMETERS}.nodeType`]: 'kubernetes', + [`${ALERT_RULE_PARAMETERS}.nodeType`]: 'host', [`${ALERT_RULE_PARAMETERS}.criteria.metric`]: ['cpu'], [`host.name`]: ['my-host'], } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/host-detail/my-host?from=1640995200000&to=1640996100000' + "/node-mock/host/my-host?receivedParams=(alertMetric:cpu,dateRange:(from:'2022-01-01T00:00:00.000Z',to:'2022-01-01T00:15:00.000Z'))" + ); + }); + + it('should point to asset details when nodeType is container and container.id is present', () => { + const fields = { + [TIMESTAMP]: '2022-01-01T00:00:00.000Z', + [`${ALERT_RULE_PARAMETERS}.nodeType`]: 'container', + [`${ALERT_RULE_PARAMETERS}.criteria.metric`]: ['cpu'], + [`container.id`]: ['my-container'], + } as unknown as ParsedTechnicalFields & Record; + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1); + expect(url).toEqual( + "/node-mock/container/my-container?receivedParams=(alertMetric:cpu,dateRange:(from:'2022-01-01T00:00:00.000Z',to:'2022-01-01T00:15:00.000Z'))" ); }); @@ -140,9 +203,14 @@ describe('Inventory Threshold Rule', () => { _id: 'eaa439aa-a4bb-4e7c-b7f8-fbe532ca7366', _index: '.internal.alerts-observability.metrics.alerts-default-000001', } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockInventoryLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/inventory?customMetric=%28aggregation%3Aavg%2Cfield%3Asystem.cpu.user.pct%2Cid%3Aalert-custom-metric%2Ctype%3Acustom%29&metric=%28aggregation%3Aavg%2Cfield%3Asystem.cpu.user.pct%2Cid%3Aalert-custom-metric%2Ctype%3Acustom%29&nodeType=host×tamp=1640995200000' + "/inventory-mock?receivedParams=(customMetric:'(aggregation:avg,field:system.cpu.user.pct,id:alert-custom-metric,type:custom)',metric:'(aggregation:avg,field:system.cpu.user.pct,id:alert-custom-metric,type:custom)',nodeType:host,timestamp:1640995200000)" ); }); @@ -165,32 +233,75 @@ describe('Inventory Threshold Rule', () => { _id: 'eaa439aa-a4bb-4e7c-b7f8-fbe532ca7366', _index: '.internal.alerts-observability.metrics.alerts-default-000001', } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockInventoryLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/inventory?customMetric=&metric=%28type%3Acpu%29&nodeType=host×tamp=1640995200000' + "/inventory-mock?receivedParams=(customMetric:'',metric:'(type:cpu)',nodeType:host,timestamp:1640995200000)" ); }); }); }); describe('Metrics Rule', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + describe('getMetricsViewInAppUrl', () => { - it('should point to host-details when host.name is present', () => { + it('should point to host details when host.name is present', () => { const fields = { [TIMESTAMP]: '2022-01-01T00:00:00.000Z', [`host.name`]: ['my-host'], } as unknown as ParsedTechnicalFields & Record; - const url = getMetricsViewInAppUrl(fields); + const url = getMetricsViewInAppUrl({ + fields, + assetDetailsLocator: mockAssetDetailsLocator, + groupBy: ['host.name'], + }); + expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1); + expect(url).toEqual( + "/node-mock/host/my-host?receivedParams=(dateRange:(from:'2022-01-01T00:00:00.000Z',to:'2022-01-01T00:15:00.000Z'))" + ); + }); + + it('should point to container details when host.name is present', () => { + const fields = { + [TIMESTAMP]: '2022-01-01T00:00:00.000Z', + [`container.id`]: ['my-host-5xyz'], + } as unknown as ParsedTechnicalFields & Record; + const url = getMetricsViewInAppUrl({ + fields, + assetDetailsLocator: mockAssetDetailsLocator, + groupBy: ['container.id'], + }); + expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/host-detail/my-host?from=1640995200000&to=1640996100000' + "/node-mock/container/my-host-5xyz?receivedParams=(dateRange:(from:'2022-01-01T00:00:00.000Z',to:'2022-01-01T00:15:00.000Z'))" ); }); + it('should point to metrics when group by field is not supported by the asset details', () => { + const fields = { + [TIMESTAMP]: '2022-01-01T00:00:00.000Z', + [`host.name`]: ['my-host'], + } as unknown as ParsedTechnicalFields & Record; + const url = getMetricsViewInAppUrl({ + fields, + assetDetailsLocator: mockAssetDetailsLocator, + groupBy: ['kubernetes.pod.name'], + }); + expect(url).toEqual('/app/metrics/explorer'); + }); + it('should point to metrics explorer', () => { const fields = { [TIMESTAMP]: '2022-01-01T00:00:00.000Z', } as unknown as ParsedTechnicalFields & Record; - const url = getMetricsViewInAppUrl(fields); + const url = getMetricsViewInAppUrl({ fields }); expect(url).toEqual('/app/metrics/explorer'); }); }); diff --git a/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts b/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts index 06d34a83f123..3dc84f040614 100644 --- a/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts +++ b/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts @@ -6,16 +6,22 @@ */ import { ALERT_RULE_PARAMETERS, TIMESTAMP } from '@kbn/rule-data-utils'; +import moment from 'moment'; import { encode } from '@kbn/rison'; -import { stringify } from 'query-string'; import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields'; -import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; +import { type InventoryItemType, findInventoryModel } from '@kbn/metrics-data-access-plugin/common'; +import type { LocatorPublic } from '@kbn/share-plugin/common'; import { - fifteenMinutesInMilliseconds, - HOST_NAME_FIELD, - LINK_TO_INVENTORY, - METRICS_EXPLORER_URL, -} from '../../constants'; + type AssetDetailsLocatorParams, + type InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; +import { castArray } from 'lodash'; +import { fifteenMinutesInMilliseconds, METRICS_EXPLORER_URL } from '../../constants'; +import { SupportedAssetTypes } from '../../asset_details/types'; + +const ALERT_RULE_PARAMTERS_INVENTORY_METRIC_ID = `${ALERT_RULE_PARAMETERS}.criteria.metric`; +export const ALERT_RULE_PARAMETERS_NODE_TYPE = `${ALERT_RULE_PARAMETERS}.nodeType`; +const CUSTOM_METRIC_TYPE = 'custom'; export const flatAlertRuleParams = (params: {}, pKey = ''): Record => { return Object.entries(params).reduce((acc, [key, field]) => { @@ -32,10 +38,18 @@ export const flatAlertRuleParams = (params: {}, pKey = ''): Record); }; -export const getInventoryViewInAppUrl = ( - fields: ParsedTechnicalFields & Record -): string => { - let inventoryFields = fields; +export const getInventoryViewInAppUrl = ({ + fields, + assetDetailsLocator, + inventoryLocator, +}: { + fields: ParsedTechnicalFields & Record; + assetDetailsLocator?: LocatorPublic; + inventoryLocator?: LocatorPublic; +}): string => { + if (!assetDetailsLocator || !inventoryLocator) { + return ''; + } /* Temporary Solution -> https://github.com/elastic/kibana/issues/137033 * In the alert table from timelines plugin (old table), we are using an API who is flattening all the response @@ -45,75 +59,131 @@ export const getInventoryViewInAppUrl = ( * triggersActionUI then we will stop using this flattening way and we will update the code to work with fields API, * it will be less magic. */ - if (fields[ALERT_RULE_PARAMETERS]) { - inventoryFields = { - ...fields, - ...flatAlertRuleParams(fields[ALERT_RULE_PARAMETERS] as {}, ALERT_RULE_PARAMETERS), - }; + const inventoryFields = fields[ALERT_RULE_PARAMETERS] + ? { + ...fields, + ...flatAlertRuleParams(fields[ALERT_RULE_PARAMETERS] as {}, ALERT_RULE_PARAMETERS), + } + : fields; + + const nodeType = castArray(inventoryFields[ALERT_RULE_PARAMETERS_NODE_TYPE])[0]; + + if (!nodeType) { + return ''; } - const nodeTypeField = `${ALERT_RULE_PARAMETERS}.nodeType`; - const nodeType = inventoryFields[nodeTypeField] as InventoryItemType; - const hostName = inventoryFields[HOST_NAME_FIELD]; + const assetIdField = findInventoryModel(nodeType).fields.id; + const assetId = inventoryFields[assetIdField]; + const assetDetailsSupported = Object.values(SupportedAssetTypes).includes( + nodeType as SupportedAssetTypes + ); + const criteriaMetric = inventoryFields[ALERT_RULE_PARAMTERS_INVENTORY_METRIC_ID][0]; - if (nodeType) { - if (hostName) { - return getLinkToHostDetails({ hostName, timestamp: inventoryFields[TIMESTAMP] }); - } - const linkToParams = { - nodeType: inventoryFields[nodeTypeField][0], - timestamp: Date.parse(inventoryFields[TIMESTAMP]), - customMetric: '', - metric: '', - }; - - // We always pick the first criteria metric for the URL - const criteriaMetric = inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.metric`][0]; - if (criteriaMetric === 'custom') { - const criteriaCustomMetricId = - inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`][0]; - const criteriaCustomMetricAggregation = - inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`][0]; - const criteriaCustomMetricField = - inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.field`][0]; - - const customMetric = encode({ - id: criteriaCustomMetricId, - type: 'custom', - field: criteriaCustomMetricField, - aggregation: criteriaCustomMetricAggregation, - }); - linkToParams.customMetric = customMetric; - linkToParams.metric = customMetric; - } else { - linkToParams.metric = encode({ type: criteriaMetric }); - } - return `${LINK_TO_INVENTORY}?${stringify(linkToParams)}`; + if (assetId && assetDetailsSupported) { + return getLinkToAssetDetails({ + assetId, + assetType: nodeType, + timestamp: inventoryFields[TIMESTAMP], + alertMetric: criteriaMetric, + assetDetailsLocator, + }); + } + + const linkToParams = { + nodeType, + timestamp: Date.parse(inventoryFields[TIMESTAMP]), + customMetric: '', + metric: '', + }; + + // We always pick the first criteria metric for the URL + + if (criteriaMetric === CUSTOM_METRIC_TYPE) { + const criteriaCustomMetricId = + inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`][0]; + const criteriaCustomMetricAggregation = + inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`][0]; + const criteriaCustomMetricField = + inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.field`][0]; + + const customMetric = encode({ + id: criteriaCustomMetricId, + type: CUSTOM_METRIC_TYPE, + field: criteriaCustomMetricField, + aggregation: criteriaCustomMetricAggregation, + }); + linkToParams.customMetric = customMetric; + linkToParams.metric = customMetric; + } else { + linkToParams.metric = encode({ type: criteriaMetric }); } - return LINK_TO_INVENTORY; + return inventoryLocator.getRedirectUrl({ + ...linkToParams, + }); }; -export const getMetricsViewInAppUrl = (fields: ParsedTechnicalFields & Record) => { - const hostName = fields[HOST_NAME_FIELD]; - const timestamp = fields[TIMESTAMP]; +export const getMetricsViewInAppUrl = ({ + fields, + groupBy, + assetDetailsLocator, +}: { + fields: ParsedTechnicalFields & Record; + groupBy?: string[]; + assetDetailsLocator?: LocatorPublic; +}) => { + if (!groupBy || !assetDetailsLocator) { + return METRICS_EXPLORER_URL; + } + + // creates an object of asset details supported assetType by their assetId field name + const assetTypeByAssetId = Object.values(SupportedAssetTypes).reduce((acc, curr) => { + acc[findInventoryModel(curr).fields.id] = curr; + return acc; + }, {} as Record); + + // detemines if the groupBy has a field that the asset details supports + const supportedAssetId = groupBy?.find((field) => !!assetTypeByAssetId[field]); + // assigns a nodeType if the groupBy field is supported by asset details + const supportedAssetType = supportedAssetId ? assetTypeByAssetId[supportedAssetId] : undefined; + + if (supportedAssetType) { + const assetId = fields[findInventoryModel(supportedAssetType).fields.id]; + const timestamp = fields[TIMESTAMP]; - return hostName ? getLinkToHostDetails({ hostName, timestamp }) : METRICS_EXPLORER_URL; + return getLinkToAssetDetails({ + assetId, + assetType: supportedAssetType, + timestamp, + assetDetailsLocator, + }); + } else { + return METRICS_EXPLORER_URL; + } }; -export function getLinkToHostDetails({ - hostName, +function getLinkToAssetDetails({ + assetId, + assetType, timestamp, + alertMetric, + assetDetailsLocator, }: { - hostName: string; + assetId: string; + assetType: InventoryItemType; timestamp: string; + alertMetric?: string; + assetDetailsLocator: LocatorPublic; }): string { - const queryParams = { - from: Date.parse(timestamp), - to: Date.parse(timestamp) + fifteenMinutesInMilliseconds, - }; - - const encodedParams = encode(stringify(queryParams)); - - return `/app/metrics/link-to/host-detail/${hostName}?${encodedParams}`; + return assetDetailsLocator.getRedirectUrl({ + assetId, + assetType, + assetDetails: { + dateRange: { + from: timestamp, + to: moment(timestamp).add(fifteenMinutesInMilliseconds, 'ms').toISOString(), + }, + ...(alertMetric && alertMetric !== CUSTOM_METRIC_TYPE ? { alertMetric } : undefined), + }, + }); } diff --git a/x-pack/plugins/observability_solution/infra/common/asset_details/types.ts b/x-pack/plugins/observability_solution/infra/common/asset_details/types.ts new file mode 100644 index 000000000000..685b2bcacb2e --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/common/asset_details/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export enum SupportedAssetTypes { + container = 'container', + host = 'host', +} diff --git a/x-pack/plugins/observability_solution/infra/common/constants.ts b/x-pack/plugins/observability_solution/infra/common/constants.ts index 0bbabffbb17c..63dfa663ce25 100644 --- a/x-pack/plugins/observability_solution/infra/common/constants.ts +++ b/x-pack/plugins/observability_solution/infra/common/constants.ts @@ -43,7 +43,6 @@ export const O11Y_AAD_FIELDS = [ 'tags', ]; -export const LINK_TO_INVENTORY = '/app/metrics/link-to/inventory'; export const METRICS_EXPLORER_URL = '/app/metrics/explorer'; export const fifteenMinutesInMilliseconds = 15 * 60 * 1000; diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/inventory/index.ts b/x-pack/plugins/observability_solution/infra/public/alerting/inventory/index.ts index d95440d7cac7..0d0fd398909a 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/inventory/index.ts +++ b/x-pack/plugins/observability_solution/infra/public/alerting/inventory/index.ts @@ -9,12 +9,17 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { RuleTypeParams } from '@kbn/alerting-plugin/common'; import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public'; +import type { LocatorPublic } from '@kbn/share-plugin/common'; +import type { + AssetDetailsLocatorParams, + InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { InventoryMetricConditions, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, } from '../../../common/alerting/metrics'; import { validateMetricThreshold } from './components/validation'; -import { formatReason } from './rule_data_formatters'; +import { getRuleFormat } from './rule_data_formatters'; interface InventoryMetricRuleTypeParams extends RuleTypeParams { criteria: InventoryMetricConditions[]; @@ -50,7 +55,15 @@ const inventoryDefaultRecoveryMessage = i18n.translate( } ); -export function createInventoryMetricRuleType(): ObservabilityRuleTypeModel { +export function createInventoryMetricRuleType({ + assetDetailsLocator, + inventoryLocator, +}: { + assetDetailsLocator?: LocatorPublic; + inventoryLocator?: LocatorPublic; +}): ObservabilityRuleTypeModel { + const format = getRuleFormat({ assetDetailsLocator, inventoryLocator }); + return { id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.metrics.inventory.alertFlyout.alertDescription', { @@ -65,7 +78,7 @@ export function createInventoryMetricRuleType(): ObservabilityRuleTypeModel { - const reason = fields[ALERT_REASON] ?? '-'; +export const getRuleFormat = ({ + assetDetailsLocator, + inventoryLocator, +}: { + assetDetailsLocator?: LocatorPublic; + inventoryLocator?: LocatorPublic; +}): ObservabilityRuleTypeFormatter => { + return ({ fields }) => { + const reason = fields[ALERT_REASON] ?? '-'; - return { - reason, - link: getInventoryViewInAppUrl(fields), + return { + reason, + link: getInventoryViewInAppUrl({ fields, assetDetailsLocator, inventoryLocator }), + hasBasePath: true, + }; }; }; diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/index.ts b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/index.ts index 362c6a500dd8..a37d14c061f7 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/index.ts +++ b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/index.ts @@ -9,12 +9,14 @@ import { i18n } from '@kbn/i18n'; import { lazy } from 'react'; import { RuleTypeParams } from '@kbn/alerting-plugin/common'; import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public'; +import { LocatorPublic } from '@kbn/share-plugin/common'; +import { AssetDetailsLocatorParams } from '@kbn/observability-shared-plugin/common'; import { MetricExpressionParams, METRIC_THRESHOLD_ALERT_TYPE_ID, } from '../../../common/alerting/metrics'; import { validateMetricThreshold } from './components/validation'; -import { formatReason } from './rule_data_formatters'; +import { getRuleFormat } from './rule_data_formatters'; export interface MetricThresholdRuleTypeParams extends RuleTypeParams { criteria: MetricExpressionParams[]; @@ -50,7 +52,11 @@ const metricThresholdDefaultRecoveryMessage = i18n.translate( } ); -export function createMetricThresholdRuleType(): ObservabilityRuleTypeModel { +export function createMetricThresholdRuleType({ + assetDetailsLocator, +}: { + assetDetailsLocator?: LocatorPublic; +}): ObservabilityRuleTypeModel { return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.metrics.alertFlyout.alertDescription', { @@ -65,7 +71,7 @@ export function createMetricThresholdRuleType(): ObservabilityRuleTypeModel import('./components/alert_details_app_section')), priority: 10, }; diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/rule_data_formatters.ts b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/rule_data_formatters.ts index 75d5bceb6132..85169903c68d 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/rule_data_formatters.ts +++ b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/rule_data_formatters.ts @@ -5,14 +5,33 @@ * 2.0. */ -import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils'; import { ObservabilityRuleTypeFormatter } from '@kbn/observability-plugin/public'; +import { LocatorPublic } from '@kbn/share-plugin/common'; +import type { AssetDetailsLocatorParams } from '@kbn/observability-shared-plugin/common'; +import { castArray } from 'lodash'; +import { METRICS_EXPLORER_URL } from '../../../common/constants'; import { getMetricsViewInAppUrl } from '../../../common/alerting/metrics/alert_link'; -export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => { - const reason = fields[ALERT_REASON] ?? '-'; - return { - reason, - link: getMetricsViewInAppUrl(fields), +export const getRuleFormat = ({ + assetDetailsLocator, +}: { + assetDetailsLocator?: LocatorPublic; +}): ObservabilityRuleTypeFormatter => { + return ({ fields }) => { + const reason = fields[ALERT_REASON] ?? '-'; + const parameters = fields[ALERT_RULE_PARAMETERS]; + + const link = getMetricsViewInAppUrl({ + fields, + groupBy: castArray(parameters?.groupBy as string[] | string), + assetDetailsLocator, + }); + + return { + reason, + link, + hasBasePath: link !== METRICS_EXPLORER_URL, + }; }; }; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/constants.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/constants.ts index e189c8e3524f..3b3db1b21bd0 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/constants.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/constants.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { SupportedAssetTypes } from '../../../common/asset_details/types'; import type { DockerContainerMetrics, KubernetesContainerMetrics } from './charts/types'; -import { INTEGRATION_NAME, ASSET_DETAILS_ASSET_TYPE } from './types'; +import { IntegrationEventModules } from './types'; export const ASSET_DETAILS_FLYOUT_COMPONENT_NAME = 'infraAssetDetailsFlyout'; export const ASSET_DETAILS_PAGE_COMPONENT_NAME = 'infraAssetDetailsPage'; @@ -15,16 +16,16 @@ export const APM_HOST_FILTER_FIELD = 'host.hostname'; export const APM_CONTAINER_FILTER_FIELD = 'container.id'; export const APM_FILTER_FIELD_PER_ASSET_TYPE = { - [ASSET_DETAILS_ASSET_TYPE.container]: APM_CONTAINER_FILTER_FIELD, - [ASSET_DETAILS_ASSET_TYPE.host]: APM_HOST_FILTER_FIELD, + [SupportedAssetTypes.container]: APM_CONTAINER_FILTER_FIELD, + [SupportedAssetTypes.host]: APM_HOST_FILTER_FIELD, }; export const ASSET_DETAILS_URL_STATE_KEY = 'assetDetails'; export const INTEGRATIONS = { - [INTEGRATION_NAME.kubernetesNode]: 'kubernetes.node', - [INTEGRATION_NAME.kubernetesContainer]: 'kubernetes.container', - [INTEGRATION_NAME.docker]: 'docker', + [IntegrationEventModules.kubernetesNode]: 'kubernetes.node', + [IntegrationEventModules.kubernetesContainer]: 'kubernetes.container', + [IntegrationEventModules.docker]: 'docker', }; export const DOCKER_METRIC_TYPES: DockerContainerMetrics[] = ['cpu', 'memory', 'network', 'disk']; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts.tsx new file mode 100644 index 000000000000..135c7e2ce77e --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts.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 { + type SnapshotMetricType, + findInventoryModel, + type InventoryModels, + InventoryItemType, +} from '@kbn/metrics-data-access-plugin/common'; +import { useAssetDetailsUrlState } from '../hooks/use_asset_details_url_state'; +import { useAssetDetailsRenderPropsContext } from '../hooks/use_asset_details_render_props'; +import { LegacyAlertMetricCallout } from './callouts/legacy_metric_callout'; +import { ContentTabIds } from '../types'; + +const INCOMING_ALERT_CALLOUT_VISIBLE_FOR = [ContentTabIds.OVERVIEW, ContentTabIds.METRICS]; + +const isSnapshotMetricType = ( + inventoryModel: InventoryModels, + value?: string +): value is SnapshotMetricType => { + return !!value && !!inventoryModel.metrics.snapshot[value]; +}; + +export const Callouts = () => { + const { asset } = useAssetDetailsRenderPropsContext(); + const [state] = useAssetDetailsUrlState(); + + const assetConfig = findInventoryModel(asset.type); + const alertMetric = isSnapshotMetricType(assetConfig, state?.alertMetric) + ? state?.alertMetric + : undefined; + + if (asset.type === 'host' && alertMetric && assetConfig.legacyMetrics?.includes(alertMetric)) { + return ( + + ); + } + + return null; +}; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts/legacy_metric_callout.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts/legacy_metric_callout.tsx new file mode 100644 index 000000000000..f38897155fac --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts/legacy_metric_callout.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 { FormattedMessage } from '@kbn/i18n-react'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; +import { InventoryItemType, SnapshotMetricType } from '@kbn/metrics-data-access-plugin/common'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { HOST_METRICS_DOC_HREF } from '../../../../common/visualizations'; +import { toMetricOpt } from '../../../../../common/snapshot_metric_i18n'; +import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props'; +import { ContentTabIds } from '../../types'; +import { useTabSwitcherContext } from '../../hooks/use_tab_switcher'; + +const DISMISSAL_LEGACY_ALERT_METRIC_STORAGE_KEY = 'infraAssetDetails:legacy_alert_metric_dismissed'; + +export const LegacyAlertMetricCallout = ({ + visibleFor, + metric, +}: { + visibleFor: ContentTabIds[]; + metric: SnapshotMetricType; +}) => { + const { activeTabId } = useTabSwitcherContext(); + const { asset } = useAssetDetailsRenderPropsContext(); + const [isDismissed, setDismissed] = useLocalStorage( + `${DISMISSAL_LEGACY_ALERT_METRIC_STORAGE_KEY}_${metric}`, + false + ); + + const onDismiss = () => { + setDismissed(true); + }; + + const metricLabel = toMetricOpt(metric, asset.id as InventoryItemType); + const hideCallout = isDismissed || !visibleFor.includes(activeTabId as ContentTabIds); + + if (hideCallout || !metricLabel) { + return null; + } + + return ( + + } + data-test-subj="infraAssetDetailsLegacyMetricAlertCallout" + onDismiss={onDismiss} + > + + + + ), + }} + /> + + ); +}; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx index 52bff06e75a3..eadcc74c5a8d 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx @@ -22,22 +22,30 @@ import { Profiling, } from '../tabs'; import { ContentTabIds } from '../types'; +import { Callouts } from './callouts'; export const Content = () => { return ( - + + + + + + + + diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts index f7d8ca564f29..d0694ef7f207 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts @@ -99,6 +99,7 @@ const AssetDetailsUrlStateRT = rt.partial({ profilingSearch: rt.string, alertStatus: AlertStatusRT, dashboardId: rt.string, + alertMetric: rt.string, }); const AssetDetailsUrlRT = rt.union([AssetDetailsUrlStateRT, rt.null]); diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx index be98902ad9c5..a3d94c5c6e14 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx @@ -15,7 +15,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useUiSetting } from '@kbn/kibana-react-plugin/public'; import { enableInfrastructureAssetCustomDashboards } from '@kbn/observability-plugin/common'; import { useLinkProps } from '@kbn/observability-shared-plugin/public'; -import { capitalize } from 'lodash'; +import { capitalize, isEmpty } from 'lodash'; import React, { useCallback, useMemo } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { usePluginConfig } from '../../../containers/plugin_config_context'; @@ -62,7 +62,7 @@ export const useTemplateHeaderBreadcrumbs = () => { const breadcrumbs: EuiBreadcrumbsProps['breadcrumbs'] = // If there is a state object in location, it's persisted in case the page is opened in a new tab or after page refresh // With that, we can show the return button. Otherwise, it will be hidden (ex: the user opened a shared URL or opened the page from their bookmarks) - location.state || history.length > 1 + !isEmpty(location.state) || history.length > 1 ? [ { text: ( diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx index 5ac8809be544..346acb6d8a16 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx @@ -5,16 +5,13 @@ * 2.0. */ -import { EuiFlexGroup } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { SYSTEM_INTEGRATION } from '../../../../common/constants'; import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; import { useParentBreadcrumbResolver } from '../../../hooks/use_parent_breadcrumb_resolver'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; -import { InfraLoadingPanel } from '../../loading'; import { ASSET_DETAILS_PAGE_COMPONENT_NAME } from '../constants'; import { Content } from '../content/content'; import { useAssetDetailsRenderPropsContext } from '../hooks/use_asset_details_render_props'; @@ -86,7 +83,7 @@ export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => { onboardingFlow={asset.type === 'host' ? OnboardingFlow.Hosts : OnboardingFlow.Infra} dataAvailabilityModules={DATA_AVAILABILITY_PER_TYPE[asset.type] || undefined} pageHeader={{ - pageTitle: asset.name, + pageTitle: loading ? : asset.name, tabs: tabEntries, rightSideItems, breadcrumbs: headerBreadcrumbs, @@ -94,24 +91,7 @@ export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => { data-component-name={ASSET_DETAILS_PAGE_COMPONENT_NAME} data-asset-type={asset.type} > - {loading ? ( - - - - ) : ( - - )} + ); }; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts index 01700206285e..064b82094a50 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts @@ -94,13 +94,8 @@ export interface RouteState { export type DataViewOrigin = 'logs' | 'metrics'; -export enum INTEGRATION_NAME { +export enum IntegrationEventModules { kubernetesNode = 'kubernetesNode', kubernetesContainer = 'kubernetesContainer', docker = 'docker', } - -export enum ASSET_DETAILS_ASSET_TYPE { - container = 'container', - host = 'host', -} diff --git a/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx b/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx index ca271b46146c..d8522aa0f4d5 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx @@ -10,7 +10,10 @@ import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import type { SerializableRecord } from '@kbn/utility-types'; -import { ASSET_DETAILS_LOCATOR_ID } from '@kbn/observability-shared-plugin/common'; +import { + ASSET_DETAILS_LOCATOR_ID, + type AssetDetailsLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { useHostIpToName } from './use_host_ip_to_name'; import { LoadingPage } from '../../components/loading_page'; import { Error } from '../error'; @@ -32,7 +35,7 @@ export const RedirectToHostDetailViaIP = ({ const { services: { share }, } = useKibanaContextForPlugin(); - const baseLocator = share.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const baseLocator = share.url.locators.get(ASSET_DETAILS_LOCATOR_ID); const { error, name } = useHostIpToName(hostIp, (metricsView && metricsView.indices) || null); diff --git a/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx b/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx index 714be106fad3..d0bac8d8c9bf 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx @@ -14,7 +14,8 @@ import { type AssetDetailsLocatorParams, } from '@kbn/observability-shared-plugin/common'; import type { SerializableRecord } from '@kbn/utility-types'; -import { AssetDetailsUrlState } from '../../components/asset_details/types'; +import { SupportedAssetTypes } from '../../../common/asset_details/types'; +import { type AssetDetailsUrlState } from '../../components/asset_details/types'; import { ASSET_DETAILS_URL_STATE_KEY } from '../../components/asset_details/constants'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; @@ -22,7 +23,7 @@ export const REDIRECT_NODE_DETAILS_FROM_KEY = 'from'; export const REDIRECT_NODE_DETAILS_TO_KEY = 'to'; export const REDIRECT_ASSET_DETAILS_KEY = 'assetDetails'; -const getHostDetailSearch = (queryParams: URLSearchParams) => { +const getAssetDetailsQueryParams = (queryParams: URLSearchParams) => { const from = queryParams.get(REDIRECT_NODE_DETAILS_FROM_KEY); const to = queryParams.get(REDIRECT_NODE_DETAILS_TO_KEY); const assetDetailsParam = queryParams.get(REDIRECT_ASSET_DETAILS_KEY); @@ -59,7 +60,9 @@ const getNodeDetailSearch = (queryParams: URLSearchParams) => { }; export const getSearchParams = (nodeType: InventoryItemType, queryParams: URLSearchParams) => - nodeType === 'host' ? getHostDetailSearch(queryParams) : getNodeDetailSearch(queryParams); + Object.values(SupportedAssetTypes).includes(nodeType as SupportedAssetTypes) + ? getAssetDetailsQueryParams(queryParams) + : getNodeDetailSearch(queryParams); export const RedirectToNodeDetail = () => { const { diff --git a/x-pack/plugins/observability_solution/infra/public/plugin.ts b/x-pack/plugins/observability_solution/infra/public/plugin.ts index 6a4e813064ee..86d5e7816ce7 100644 --- a/x-pack/plugins/observability_solution/infra/public/plugin.ts +++ b/x-pack/plugins/observability_solution/infra/public/plugin.ts @@ -23,6 +23,12 @@ import type { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { apiCanAddNewPanel } from '@kbn/presentation-containers'; import { IncompatibleActionError, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public'; import { COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public'; +import { + ASSET_DETAILS_LOCATOR_ID, + INVENTORY_LOCATOR_ID, + type AssetDetailsLocatorParams, + type InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import type { InfraPublicConfig } from '../common/plugin_config_types'; import { createInventoryMetricRuleType } from './alerting/inventory'; import { createLogThresholdRuleType } from './alerting/log_threshold'; @@ -80,12 +86,17 @@ export class Plugin implements InfraClientPluginClass { id: ObservabilityTriggerId.LogEntryContextMenu, }); + const assetDetailsLocator = + pluginsSetup.share.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const inventoryLocator = + pluginsSetup.share.url.locators.get(INVENTORY_LOCATOR_ID); + pluginsSetup.observability.observabilityRuleTypeRegistry.register( - createInventoryMetricRuleType() + createInventoryMetricRuleType({ assetDetailsLocator, inventoryLocator }) ); pluginsSetup.observability.observabilityRuleTypeRegistry.register( - createMetricThresholdRuleType() + createMetricThresholdRuleType({ assetDetailsLocator }) ); if (this.config.featureFlags.logsUIEnabled) { diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/common/utils.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/common/utils.ts index 0de0a5a0797b..73a7ed749446 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/common/utils.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/common/utils.ts @@ -21,7 +21,13 @@ import { set } from '@kbn/safer-lodash-set'; import { Alert } from '@kbn/alerts-as-data-utils'; import { type Group } from '@kbn/observability-alerting-rule-utils'; import { ParsedExperimentalFields } from '@kbn/rule-registry-plugin/common/parse_experimental_fields'; +import type { LocatorPublic } from '@kbn/share-plugin/common'; +import type { + AssetDetailsLocatorParams, + InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { + ALERT_RULE_PARAMETERS_NODE_TYPE, getInventoryViewInAppUrl, getMetricsViewInAppUrl, } from '../../../../common/alerting/metrics/alert_link'; @@ -130,6 +136,8 @@ export const getInventoryViewInAppUrlWithSpaceId = ({ spaceId, timestamp, hostName, + assetDetailsLocator, + inventoryLocator, }: { basePath: IBasePath; criteria: InventoryMetricConditions[]; @@ -137,6 +145,8 @@ export const getInventoryViewInAppUrlWithSpaceId = ({ spaceId: string; timestamp: string; hostName?: string; + assetDetailsLocator?: LocatorPublic; + inventoryLocator?: LocatorPublic; }) => { const { metric, customMetric } = criteria[0]; @@ -145,7 +155,7 @@ export const getInventoryViewInAppUrlWithSpaceId = ({ [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`]: [customMetric?.id], [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`]: [customMetric?.aggregation], [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.field`]: [customMetric?.field], - [`${ALERT_RULE_PARAMETERS}.nodeType`]: [nodeType], + [ALERT_RULE_PARAMETERS_NODE_TYPE]: [nodeType], [TIMESTAMP]: timestamp, [HOST_NAME]: hostName, }; @@ -153,7 +163,11 @@ export const getInventoryViewInAppUrlWithSpaceId = ({ return addSpaceIdToPath( basePath.publicBaseUrl, spaceId, - getInventoryViewInAppUrl(parseTechnicalFields(fields, true)) + getInventoryViewInAppUrl({ + fields: parseTechnicalFields(fields, true), + assetDetailsLocator, + inventoryLocator, + }) ); }; @@ -161,22 +175,27 @@ export const getMetricsViewInAppUrlWithSpaceId = ({ basePath, spaceId, timestamp, - hostName, + groupBy, + assetDetailsLocator, }: { basePath: IBasePath; spaceId: string; timestamp: string; - hostName?: string; + groupBy?: string[]; + assetDetailsLocator?: LocatorPublic; }) => { const fields = { [TIMESTAMP]: timestamp, - [HOST_NAME]: hostName, }; return addSpaceIdToPath( basePath.publicBaseUrl, spaceId, - getMetricsViewInAppUrl(parseTechnicalFields(fields, true)) + getMetricsViewInAppUrl({ + fields: parseTechnicalFields(fields, true), + groupBy, + assetDetailsLocator, + }) ); }; diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts index f76a6e82e67d..2f621d04f38d 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts @@ -23,6 +23,7 @@ import { InfraBackendLibs } from '../../infra_types'; import { infraPluginMock } from '../../../mocks'; import { logsSharedPluginMock } from '@kbn/logs-shared-plugin/server/mocks'; import { createLogSourcesServiceMock } from '@kbn/logs-data-access-plugin/common/services/log_sources_service/log_sources_service.mocks'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; jest.mock('./evaluate_condition', () => ({ evaluateCondition: jest.fn() })); @@ -136,6 +137,11 @@ const mockLibs = { publicBaseUrl: 'http://localhost:5601', prepend: (path: string) => path, }, + plugins: { + share: { + setup: sharePluginMock.createSetupContract(), + }, + }, logger, } as unknown as InfraBackendLibs; const alerts = new Map(); diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 80da1034df5a..9f8b3b6d0bfa 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -21,9 +21,20 @@ import { AlertInstanceState as AlertState, } from '@kbn/alerting-plugin/common'; import { AlertsClientError, RuleExecutorOptions, RuleTypeState } from '@kbn/alerting-plugin/server'; -import { convertToBuiltInComparators, getAlertUrl } from '@kbn/observability-plugin/common'; +import { + AlertsLocatorParams, + alertsLocatorID, + convertToBuiltInComparators, + getAlertUrl, +} from '@kbn/observability-plugin/common'; import type { InventoryItemType, SnapshotMetricType } from '@kbn/metrics-data-access-plugin/common'; import { ObservabilityMetricsAlert } from '@kbn/alerts-as-data-utils'; +import { + ASSET_DETAILS_LOCATOR_ID, + INVENTORY_LOCATOR_ID, + type AssetDetailsLocatorParams, + type InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { getOriginalActionGroup } from '../../../utils/get_original_action_group'; import { AlertStates, @@ -96,6 +107,13 @@ export const createInventoryMetricThresholdExecutor = getTimeRange, } = options; + const { share } = libs.plugins; + const alertsLocator = share.setup.url.locators.get(alertsLocatorID); + const assetDetailsLocator = + share.setup.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const inventoryLocator = + share.setup.url.locators.get(INVENTORY_LOCATOR_ID); + const startTime = Date.now(); const { criteria, filterQuery, sourceId = 'default', nodeType, alertOnNoData } = params; @@ -141,7 +159,7 @@ export const createInventoryMetricThresholdExecutor = uuid, spaceId, indexedStartedAt, - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), alertState: stateToAlertMessage[AlertStates.ERROR], @@ -156,6 +174,8 @@ export const createInventoryMetricThresholdExecutor = nodeType, timestamp: indexedStartedAt, spaceId, + assetDetailsLocator, + inventoryLocator, }), }, }); @@ -293,7 +313,7 @@ export const createInventoryMetricThresholdExecutor = uuid, spaceId, indexedStartedAt, - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), alertState: stateToAlertMessage[nextState], @@ -312,6 +332,8 @@ export const createInventoryMetricThresholdExecutor = timestamp: indexedStartedAt, spaceId, hostName: additionalContext?.host?.name, + assetDetailsLocator, + inventoryLocator, }), ...additionalContext, }; @@ -347,7 +369,7 @@ export const createInventoryMetricThresholdExecutor = alertUuid, spaceId, indexedStartedAt, - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), alertState: stateToAlertMessage[AlertStates.OK], @@ -362,6 +384,8 @@ export const createInventoryMetricThresholdExecutor = timestamp: indexedStartedAt, spaceId, hostName: additionalContext?.host?.name, + assetDetailsLocator, + inventoryLocator, }), originalAlertState: translateActionGroupToAlertState(originalActionGroup), originalAlertStateWasALERT: originalActionGroup === FIRED_ACTIONS_ID, diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 9b562e3d4914..44cd61943df4 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -31,6 +31,7 @@ import { ALERT_GROUP, } from '@kbn/rule-data-utils'; import { type Group } from '@kbn/observability-alerting-rule-utils'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; jest.mock('./lib/evaluate_rule', () => ({ evaluateRule: jest.fn() })); @@ -2473,6 +2474,11 @@ const mockLibs: any = { publicBaseUrl: 'http://localhost:5601', prepend: (path: string) => path, }, + plugins: { + share: { + setup: sharePluginMock.createSetupContract(), + }, + }, logger, }; diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 4c0a19ae2e51..258a410d4775 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -12,7 +12,7 @@ import { ALERT_GROUP, ALERT_REASON, } from '@kbn/rule-data-utils'; -import { isEqual } from 'lodash'; +import { castArray, isEqual } from 'lodash'; import { ActionGroupIdsOf, AlertInstanceContext as AlertContext, @@ -20,11 +20,20 @@ import { RecoveredActionGroup, } from '@kbn/alerting-plugin/common'; import { AlertsClientError, RuleExecutorOptions, RuleTypeState } from '@kbn/alerting-plugin/server'; -import { TimeUnitChar, getAlertUrl } from '@kbn/observability-plugin/common'; +import { + AlertsLocatorParams, + TimeUnitChar, + alertsLocatorID, + getAlertUrl, +} from '@kbn/observability-plugin/common'; import { ObservabilityMetricsAlert } from '@kbn/alerts-as-data-utils'; import { COMPARATORS } from '@kbn/alerting-comparators'; import { getEcsGroups, type Group } from '@kbn/observability-alerting-rule-utils'; import { convertToBuiltInComparators } from '@kbn/observability-plugin/common/utils/convert_legacy_outside_comparator'; +import { + ASSET_DETAILS_LOCATOR_ID, + AssetDetailsLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { getOriginalActionGroup } from '../../../utils/get_original_action_group'; import { AlertStates } from '../../../../common/alerting/metrics'; import { createFormatter } from '../../../../common/formatters'; @@ -111,6 +120,11 @@ export const createMetricThresholdExecutor = MetricThresholdAlert > ) => { + const { share } = libs.plugins; + const alertsLocator = share.setup.url.locators.get(alertsLocatorID); + const assetDetailsLocator = + share.setup.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const startTime = Date.now(); const { @@ -126,6 +140,8 @@ export const createMetricThresholdExecutor = const { criteria } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); + const groupBy = castArray(params.groupBy); + const logger = createScopedLogger(libs.logger, 'metricThresholdRule', { alertId: ruleId, executionId, @@ -167,7 +183,7 @@ export const createMetricThresholdExecutor = uuid, spaceId, start ?? startedAt.toISOString(), - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), }, @@ -203,6 +219,8 @@ export const createMetricThresholdExecutor = basePath: libs.basePath, spaceId, timestamp, + groupBy, + assetDetailsLocator, }), }; @@ -217,7 +235,7 @@ export const createMetricThresholdExecutor = state: { lastRunTimestamp: startedAt.valueOf(), missingGroups: [], - groupBy: params.groupBy, + groupBy, filterQuery: params.filterQuery, }, }; @@ -410,7 +428,8 @@ export const createMetricThresholdExecutor = basePath: libs.basePath, spaceId, timestamp, - hostName: additionalContext?.host?.name, + groupBy, + assetDetailsLocator, }), ...additionalContext, }; @@ -450,7 +469,7 @@ export const createMetricThresholdExecutor = alertUuid, spaceId, indexedStartedAt, - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), alertState: stateToAlertMessage[AlertStates.OK], @@ -468,7 +487,8 @@ export const createMetricThresholdExecutor = basePath: libs.basePath, spaceId, timestamp: indexedStartedAt, - hostName: additionalContext?.host?.name, + groupBy, + assetDetailsLocator, }), originalAlertState: translateActionGroupToAlertState(originalActionGroup), @@ -486,7 +506,7 @@ export const createMetricThresholdExecutor = state: { lastRunTimestamp: startedAt.valueOf(), missingGroups: [...nextMissingGroups], - groupBy: params.groupBy, + groupBy, filterQuery: params.filterQuery, }, }; diff --git a/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_apm_data_access_client.ts b/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_apm_data_access_client.ts index 1936c59d7a63..e99d57eb4d6c 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_apm_data_access_client.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_apm_data_access_client.ts @@ -28,12 +28,12 @@ export const getApmDataAccessClient = ({ request: KibanaRequest; }) => { const hasPrivileges = async () => { - const [, { apmDataAccess }] = await libs.getStartServices(); - return apmDataAccess.hasPrivileges({ request }); + const apmDataAccessStart = await libs.plugins.apmDataAccess.start(); + return apmDataAccessStart.hasPrivileges({ request }); }; const getServices = async () => { - const { apmDataAccess } = libs; + const apmDataAccess = libs.plugins.apmDataAccess.setup; const coreContext = await context.core; diff --git a/x-pack/plugins/observability_solution/infra/server/lib/infra_types.ts b/x-pack/plugins/observability_solution/infra/server/lib/infra_types.ts index 96c5cd9f311d..f13424c6331d 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/infra_types.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/infra_types.ts @@ -8,23 +8,30 @@ import type { Logger } from '@kbn/logging'; import type { IBasePath } from '@kbn/core/server'; import type { handleEsError } from '@kbn/es-ui-shared-plugin/server'; -import type { AlertsLocatorParams } from '@kbn/observability-plugin/common'; import { ObservabilityConfig } from '@kbn/observability-plugin/server'; -import type { LocatorPublic } from '@kbn/share-plugin/common'; import type { ILogsSharedLogEntriesDomain } from '@kbn/logs-shared-plugin/server'; -import type { ApmDataAccessPluginSetup } from '@kbn/apm-data-access-plugin/server'; import { RulesServiceSetup } from '../services/rules'; import { InfraConfig, InfraPluginStartServicesAccessor } from '../types'; import { KibanaFramework } from './adapters/framework/kibana_framework_adapter'; import { InfraMetricsDomain } from './domains/metrics_domain'; import { InfraSources } from './sources'; import { InfraSourceStatus } from './source_status'; +import type { InfraServerPluginSetupDeps, InfraServerPluginStartDeps } from './adapters/framework'; export interface InfraDomainLibs { logEntries: ILogsSharedLogEntriesDomain; metrics: InfraMetricsDomain; } +type Plugins = { + [key in keyof InfraServerPluginSetupDeps]: { + setup: Required[key]; + } & (key extends keyof InfraServerPluginStartDeps + ? { + start: () => Promise[key]>; + } + : {}); +}; export interface InfraBackendLibs extends InfraDomainLibs { basePath: IBasePath; configuration: InfraConfig; @@ -37,6 +44,5 @@ export interface InfraBackendLibs extends InfraDomainLibs { getStartServices: InfraPluginStartServicesAccessor; handleEsError: typeof handleEsError; logger: Logger; - alertsLocator?: LocatorPublic; - apmDataAccess: ApmDataAccessPluginSetup; + plugins: Plugins; } diff --git a/x-pack/plugins/observability_solution/infra/server/plugin.ts b/x-pack/plugins/observability_solution/infra/server/plugin.ts index 6e6f87776fbc..530dec8bc1ca 100644 --- a/x-pack/plugins/observability_solution/infra/server/plugin.ts +++ b/x-pack/plugins/observability_solution/infra/server/plugin.ts @@ -16,9 +16,9 @@ import { import { handleEsError } from '@kbn/es-ui-shared-plugin/server'; import { i18n } from '@kbn/i18n'; import { Logger } from '@kbn/logging'; -import { alertsLocatorID } from '@kbn/observability-plugin/common'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { GetMetricIndicesOptions } from '@kbn/metrics-data-access-plugin/server'; +import { mapValues } from 'lodash'; import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants'; import { publicConfigKeys } from '../common/plugin_config_types'; import { LOGS_FEATURE, METRICS_FEATURE } from './features'; @@ -212,12 +212,24 @@ export class InfraServerPlugin metrics: new InfraMetricsDomain(new KibanaMetricsAdapter(framework)), }; + // Instead of passing plugins individually to `libs` on a necessity basis, + // this provides an object with all plugins infra depends on + const libsPlugins = mapValues(plugins, (value, key) => { + return { + setup: value, + start: () => + core.getStartServices().then((services) => { + const [, pluginsStartContracts] = services; + return pluginsStartContracts[key as keyof InfraServerPluginStartDeps]; + }), + }; + }) as InfraBackendLibs['plugins']; + this.libs = { configuration: this.config, framework, sources, sourceStatus, - apmDataAccess: plugins.apmDataAccess, ...domainLibs, handleEsError, logsRules: this.logsRules.setup(core, plugins), @@ -226,7 +238,7 @@ export class InfraServerPlugin getAlertDetailsConfig: () => plugins.observability.getAlertDetailsConfig(), logger: this.logger, basePath: core.http.basePath, - alertsLocator: plugins.share.url.locators.get(alertsLocatorID), + plugins: libsPlugins, }; plugins.features.registerKibanaFeature(METRICS_FEATURE); diff --git a/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts b/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts index e962a11f9a39..86af345d5175 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts @@ -39,7 +39,7 @@ export const initServicesRoute = (libs: InfraBackendLibs) => { const client = createSearchClient(requestContext, framework, request); const soClient = savedObjects.getScopedClient(request); - const apmIndices = await libs.apmDataAccess.getApmIndices(soClient); + const apmIndices = await libs.plugins.apmDataAccess.setup.getApmIndices(soClient); const services = await getServices(client, apmIndices, { from, to, diff --git a/x-pack/plugins/observability_solution/metrics_data_access/common/index.ts b/x-pack/plugins/observability_solution/metrics_data_access/common/index.ts index 12a4b6c4e13c..b0f801d2613c 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/common/index.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/common/index.ts @@ -11,6 +11,7 @@ export { getFieldByType, findInventoryFields, metrics, + type InventoryModels, } from './inventory_models'; export { podSnapshotMetricTypes } from './inventory_models/kubernetes/pod'; diff --git a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/host/index.ts b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/host/index.ts index d77f15ad4ca3..731a84f1e83a 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/host/index.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/host/index.ts @@ -56,4 +56,5 @@ export const host: InventoryModel = { ...nginxRequireMetrics, ], tooltipMetrics: ['cpuV2', 'memory', 'txV2', 'rxV2', 'cpu', 'tx', 'rx'], + legacyMetrics: ['cpu', 'tx', 'rx'], }; diff --git a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/index.ts b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/index.ts index 41115a95405e..7dddfab59378 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/index.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/index.ts @@ -29,7 +29,7 @@ const catalog = { export const inventoryModels = Object.values(catalog); -type InventoryModels = (typeof catalog)[T]; +export type InventoryModels = (typeof catalog)[T]; export const findInventoryModel = (type: T): InventoryModels => { const model = inventoryModels.find((m) => m.id === type); diff --git a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/types.ts b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/types.ts index cc018b24eea1..042958ef7bd5 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/types.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/types.ts @@ -423,6 +423,7 @@ export interface InventoryModel { }; metrics: TMetrics; requiredMetrics: InventoryMetric[]; + legacyMetrics?: SnapshotMetricType[]; tooltipMetrics: SnapshotMetricType[]; nodeFilter?: object[]; } diff --git a/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/formatters/snapshot_metric_formats.ts b/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/formatters/snapshot_metric_formats.ts index 1715a28b1caa..81586bf25ed7 100644 --- a/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/formatters/snapshot_metric_formats.ts +++ b/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/formatters/snapshot_metric_formats.ts @@ -29,12 +29,18 @@ export const METRIC_FORMATTERS: MetricFormatters = { formatter: InfraFormatterType.percent, template: '{{value}}', }, + ['cpuV2']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, ['memory']: { formatter: InfraFormatterType.percent, template: '{{value}}', }, ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['rxV2']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['txV2']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, ['logRate']: { formatter: InfraFormatterType.abbreviatedNumber, template: '{{value}}/s', diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts index 59729aeb71f0..ca44baa6de6e 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts @@ -13,7 +13,7 @@ export type AssetDetailsLocator = LocatorPublic; export interface AssetDetailsLocatorParams extends SerializableRecord { assetType: string; assetId: string; - state?: SerializableRecord; + // asset types not migrated to use the asset details page _a?: { time?: { from?: string; @@ -23,11 +23,13 @@ export interface AssetDetailsLocatorParams extends SerializableRecord { }; assetDetails?: { tabId?: string; + name?: string; dashboardId?: string; dateRange?: { from: string; to: string; }; + alertMetric?: string; }; } @@ -36,12 +38,23 @@ export const ASSET_DETAILS_LOCATOR_ID = 'ASSET_DETAILS_LOCATOR'; export class AssetDetailsLocatorDefinition implements LocatorDefinition { public readonly id = ASSET_DETAILS_LOCATOR_ID; - public readonly getLocation = async (params: AssetDetailsLocatorParams) => { - const searchPath = rison.encodeUnknown(params._a); - const assetDetails = rison.encodeUnknown(params.assetDetails); + public readonly getLocation = async ( + params: AssetDetailsLocatorParams & { state?: SerializableRecord } + ) => { + const legacyNodeDetailsQueryParams = rison.encodeUnknown(params._a); + const assetDetailsQueryParams = rison.encodeUnknown(params.assetDetails); + + const queryParams = []; + if (assetDetailsQueryParams !== undefined) { + queryParams.push(`assetDetails=${assetDetailsQueryParams}`); + } + if (legacyNodeDetailsQueryParams !== undefined) { + queryParams.push(`_a=${legacyNodeDetailsQueryParams}`); + } + return { app: 'metrics', - path: `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}&_a=${searchPath}`, + path: `/detail/${params.assetType}/${params.assetId}?${queryParams.join('&')}`, state: params.state ? params.state : {}, }; }; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/inventory_locator.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/inventory_locator.ts index ca6e997468b5..9f4cd58188ed 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/inventory_locator.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/inventory_locator.ts @@ -40,12 +40,12 @@ export interface InventoryLocatorParams extends SerializableRecord { metric: string; // encoded value nodeType: string; region?: string; - sort: { + sort?: { by: string; direction: 'desc' | 'async'; }; - timelineOpen: boolean; - view: 'map' | 'table'; + timelineOpen?: boolean; + view?: 'map' | 'table'; state?: SerializableRecord; } diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts index c7b5e16625e0..8c7dc0d4b611 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts @@ -60,7 +60,7 @@ describe('Infra Locators', () => { expect(app).toBe('metrics'); expect(path).toBe( - `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}&_a=undefined` + `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}` ); expect(state).toBeDefined(); expect(Object.keys(state)).toHaveLength(0); @@ -72,7 +72,7 @@ describe('Infra Locators', () => { expect(app).toBe('metrics'); expect(path).toBe( - `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}&_a=undefined` + `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}` ); expect(state).toBeDefined(); expect(Object.keys(state)).toHaveLength(0); diff --git a/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts b/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts index 8773ace90c71..2ac6a69c6a0d 100644 --- a/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts @@ -19,6 +19,7 @@ import { BehaviorSubject } from 'rxjs'; import { createLazyObservabilityPageTemplate } from './components/page_template'; import { createNavigationRegistry } from './components/page_template/helpers/navigation_registry'; import { registerProfilingComponent } from './components/profiling/helpers/component_registry'; +export { updateGlobalNavigation } from './services/update_global_navigation'; import { AssetDetailsFlyoutLocatorDefinition, AssetDetailsLocatorDefinition, diff --git a/x-pack/test/functional/apps/infra/node_details.ts b/x-pack/test/functional/apps/infra/node_details.ts index fbc442b5079c..f960208ab474 100644 --- a/x-pack/test/functional/apps/infra/node_details.ts +++ b/x-pack/test/functional/apps/infra/node_details.ts @@ -7,6 +7,7 @@ import moment from 'moment'; import expect from '@kbn/expect'; +import rison from '@kbn/rison'; import { InfraSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { enableInfrastructureContainerAssetView, @@ -42,6 +43,11 @@ const END_HOST_KUBERNETES_SECTION_DATE = moment.utc( const START_CONTAINER_DATE = moment.utc(DATE_WITH_DOCKER_DATA_FROM); const END_CONTAINER_DATE = moment.utc(DATE_WITH_DOCKER_DATA_TO); +interface QueryParams { + name?: string; + alertMetric?: string; +} + export default ({ getPageObjects, getService }: FtrProviderContext) => { const observability = getService('observability'); const browser = getService('browser'); @@ -59,19 +65,24 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'timePicker', ]); - const getNodeDetailsUrl = (assetName: string) => { - const queryParams = new URLSearchParams(); - - queryParams.set('assetName', assetName); - - return queryParams.toString(); + const getNodeDetailsUrl = (queryParams?: QueryParams) => { + return rison.encodeUnknown( + Object.entries(queryParams ?? {}).reduce>((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {}) + ); }; - const navigateToNodeDetails = async (assetId: string, assetName: string, assetType: string) => { + const navigateToNodeDetails = async ( + assetId: string, + assetType: string, + queryParams?: QueryParams + ) => { await pageObjects.common.navigateToUrlWithBrowserHistory( 'infraOps', `/${NODE_DETAILS_PATH}/${assetType}/${assetId}`, - getNodeDetailsUrl(assetName), + `assetDetails=${getNodeDetailsUrl(queryParams)}`, { insertTimestamp: false, ensureCurrentUrl: false, @@ -113,7 +124,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ]); await browser.setWindowSize(1600, 1200); - await navigateToNodeDetails('Jennys-MBP.fritz.box', 'Jennys-MBP.fritz.box', 'host'); + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'host', { + name: 'Jennys-MBP.fritz.box', + }); await pageObjects.header.waitUntilLoadingHasFinished(); }); @@ -270,7 +283,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const ALL_ALERTS = ACTIVE_ALERTS + RECOVERED_ALERTS; const COLUMNS = 11; before(async () => { - await navigateToNodeDetails('demo-stack-apache-01', 'demo-stack-apache-01', 'host'); + await navigateToNodeDetails('demo-stack-apache-01', 'host', { + name: 'demo-stack-apache-01', + }); await pageObjects.header.waitUntilLoadingHasFinished(); await pageObjects.timePicker.setAbsoluteRange( @@ -282,7 +297,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); after(async () => { - await navigateToNodeDetails('Jennys-MBP.fritz.box', 'Jennys-MBP.fritz.box', 'host'); + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'host', { + name: 'Jennys-MBP.fritz.box', + }); await pageObjects.header.waitUntilLoadingHasFinished(); await pageObjects.timePicker.setAbsoluteRange( @@ -505,7 +522,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Host with alerts and no processes', () => { before(async () => { - await navigateToNodeDetails('demo-stack-mysql-01', 'demo-stack-mysql-01', 'host'); + await navigateToNodeDetails('demo-stack-mysql-01', 'host', { + name: 'demo-stack-mysql-01', + }); await pageObjects.timePicker.setAbsoluteRange( START_HOST_ALERTS_DATE.format(DATE_PICKER_FORMAT), END_HOST_ALERTS_DATE.format(DATE_PICKER_FORMAT) @@ -539,11 +558,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('#With Kubernetes section', () => { before(async () => { - await navigateToNodeDetails( - 'demo-stack-kubernetes-01', - 'demo-stack-kubernetes-01', - 'host' - ); + await navigateToNodeDetails('demo-stack-kubernetes-01', 'host', { + name: 'demo-stack-kubernetes-01', + }); await pageObjects.header.waitUntilLoadingHasFinished(); }); @@ -623,6 +640,43 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); }); + + describe('Callouts', () => { + describe('Legacy alert metric callout', () => { + [{ metric: 'cpu' }, { metric: 'rx' }, { metric: 'tx' }].forEach(({ metric }) => { + it(`Should show for: ${metric}`, async () => { + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'host', { + name: 'Jennys-MBP.fritz.box', + alertMetric: metric, + }); + await pageObjects.header.waitUntilLoadingHasFinished(); + + await retry.try(async () => { + expect(await pageObjects.assetDetails.legacyMetricAlertCalloutExists()).to.be( + true + ); + }); + }); + }); + + [{ metric: 'cpuV2' }, { metric: 'rxV2' }, { metric: 'txV2' }].forEach(({ metric }) => { + it(`Should not show for: ${metric}`, async () => { + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'host', { + name: 'Jennys-MBP.fritz.box', + alertMetric: metric, + }); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + await retry.try(async () => { + expect(await pageObjects.assetDetails.legacyMetricAlertCalloutExists()).to.be( + false + ); + }); + }); + }); + }); + }); }); describe('#Asset Type: container', () => { @@ -647,7 +701,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('when container asset view is disabled', () => { it('should show old view of container details', async () => { await setInfrastructureContainerAssetViewUiSetting(false); - await navigateToNodeDetails('container-id-0', 'container-id-0', 'container'); + await navigateToNodeDetails('container-id-0', 'container', { + name: 'container-id-0', + }); await pageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.find('metricsEmptyViewState'); }); @@ -656,7 +712,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('when container asset view is enabled', () => { before(async () => { await setInfrastructureContainerAssetViewUiSetting(true); - await navigateToNodeDetails('container-id-0', 'container-id-0', 'container'); + await navigateToNodeDetails('container-id-0', 'container', { + name: 'container-id-0', + }); await pageObjects.header.waitUntilLoadingHasFinished(); await pageObjects.timePicker.setAbsoluteRange( START_CONTAINER_DATE.format(DATE_PICKER_FORMAT), diff --git a/x-pack/test/functional/page_objects/asset_details.ts b/x-pack/test/functional/page_objects/asset_details.ts index 4a56b3dce469..4e3da871a91b 100644 --- a/x-pack/test/functional/page_objects/asset_details.ts +++ b/x-pack/test/functional/page_objects/asset_details.ts @@ -352,5 +352,10 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) { return testSubjects.click(buttonSubject); }, + + // Callouts + async legacyMetricAlertCalloutExists() { + return testSubjects.exists('infraAssetDetailsLegacyMetricAlertCallout'); + }, }; } From d971c6a10e71e71c2a40d428919e515320e791c1 Mon Sep 17 00:00:00 2001 From: Marius Iversen Date: Mon, 19 Aug 2024 12:08:53 +0200 Subject: [PATCH 02/25] [Integration Assistant] Improve sample merge functionality (#190656) ## Summary Adds a few extra checks when doing deep recursive merges, will test a few things when it comes to the overhead it adds before merging. ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../server/util/samples.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/integration_assistant/server/util/samples.ts b/x-pack/plugins/integration_assistant/server/util/samples.ts index e8489d79cdca..f6728653e75c 100644 --- a/x-pack/plugins/integration_assistant/server/util/samples.ts +++ b/x-pack/plugins/integration_assistant/server/util/samples.ts @@ -55,22 +55,25 @@ function isEmptyValue(value: unknown): boolean { function merge(target: Record, source: Record): Record { for (const [key, sourceValue] of Object.entries(source)) { - const targetValue = target[key]; - if (Array.isArray(sourceValue)) { - // Directly assign arrays - target[key] = sourceValue; - } else if ( - typeof sourceValue === 'object' && - sourceValue !== null && - !Array.isArray(targetValue) - ) { - if (typeof targetValue !== 'object' || isEmptyValue(targetValue)) { - target[key] = merge({}, sourceValue); - } else { - target[key] = merge(targetValue, sourceValue); + if (key !== '__proto__' && key !== 'constructor') { + if (Object.prototype.hasOwnProperty.call(target, key)) { + const targetValue = target[key]; + if (Array.isArray(sourceValue)) { + target[key] = sourceValue; + } else if ( + typeof sourceValue === 'object' && + sourceValue !== null && + typeof targetValue === 'object' && + targetValue !== null && + !Array.isArray(targetValue) + ) { + target[key] = merge(targetValue, sourceValue); + } else if (isEmptyValue(targetValue) && !isEmptyValue(sourceValue)) { + target[key] = sourceValue; + } + } else if (!isEmptyValue(sourceValue)) { + target[key] = sourceValue; } - } else if (!(key in target) || (isEmptyValue(targetValue) && !isEmptyValue(sourceValue))) { - target[key] = sourceValue; } } return target; From ac5d6921d07553ccd38cf01a85b1caf324177fb6 Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Mon, 19 Aug 2024 12:10:25 +0200 Subject: [PATCH 03/25] [Security Solution][Alert details] - move useBasicDataFromDetailsData hook to flyout folder (#190106) --- .../investigation_guide_view.test.tsx | 4 +- .../investigation_guide_view.tsx | 6 +- .../document_details/isolate_host/content.tsx | 2 +- .../left/components/cell_actions.tsx | 2 +- .../use_threat_intelligence_details.test.ts | 8 +- .../hooks/use_threat_intelligence_details.ts | 2 +- .../right/components/alert_description.tsx | 2 +- .../right/components/alert_header_title.tsx | 2 +- .../right/components/event_header_title.tsx | 2 +- .../right/components/header_actions.tsx | 2 +- .../right/components/highlighted_fields.tsx | 2 +- .../components/investigation_section.test.tsx | 5 + .../right/components/reason.tsx | 2 +- .../document_details/right/header.test.tsx | 4 +- .../flyout/document_details/right/header.tsx | 2 +- .../right/hooks/use_assistant.test.tsx | 6 + .../hooks/use_fetch_threat_intelligence.ts | 2 +- .../right/hooks/use_session_preview.test.tsx | 6 +- .../right/hooks/use_session_preview.ts | 2 +- .../document_details/shared/context.tsx | 2 +- .../use_basic_data_from_details_data.test.tsx | 48 +++++++ .../use_basic_data_from_details_data.tsx | 120 ++++++++++++++++++ .../hooks/use_highlighted_fields.test.tsx | 44 ++++++- .../hooks/use_investigation_guide.test.ts | 4 +- .../shared/hooks/use_investigation_guide.ts | 10 +- .../mock_data_formatted_for_field_browser.ts | 42 ++++++ .../side_panel/event_details/helpers.tsx | 113 ----------------- .../translations/translations/fr-FR.json | 5 + .../translations/translations/ja-JP.json | 5 + .../translations/translations/zh-CN.json | 5 + 30 files changed, 313 insertions(+), 148 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.test.tsx index c1a57c6a9ab7..355ad1f9129d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.test.tsx @@ -8,12 +8,12 @@ import React from 'react'; import { render } from '@testing-library/react'; import { InvestigationGuideView } from './investigation_guide_view'; -import type { GetBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import type { UseBasicDataFromDetailsDataResult } from '../../../flyout/document_details/shared/hooks/use_basic_data_from_details_data'; const defaultProps = { basicData: { ruleId: 'rule-id', - } as unknown as GetBasicDataFromDetailsData, + } as unknown as UseBasicDataFromDetailsDataResult, ruleNote: 'test note', }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx index 29e2354f7454..b3015bafe453 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx @@ -8,7 +8,7 @@ import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; import React, { createContext } from 'react'; import styled from 'styled-components'; -import type { GetBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import type { UseBasicDataFromDetailsDataResult } from '../../../flyout/document_details/shared/hooks/use_basic_data_from_details_data'; import * as i18n from './translations'; import { MarkdownRenderer } from '../markdown_editor'; import { LineClamp } from '../line_clamp'; @@ -18,13 +18,13 @@ export const Indent = styled.div` word-break: break-word; `; -export const BasicAlertDataContext = createContext>({}); +export const BasicAlertDataContext = createContext>({}); interface InvestigationGuideViewProps { /** * An object of basic fields from the event details data */ - basicData: GetBasicDataFromDetailsData; + basicData: UseBasicDataFromDetailsDataResult; /** * The markdown text of rule.note */ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/isolate_host/content.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/isolate_host/content.tsx index 658624dfdbbf..0c9f05391d82 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/isolate_host/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/isolate_host/content.tsx @@ -9,7 +9,7 @@ import type { FC } from 'react'; import React, { useCallback } from 'react'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { DocumentDetailsRightPanelKey } from '../shared/constants/panel_keys'; -import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../shared/hooks/use_basic_data_from_details_data'; import { EndpointIsolateSuccess, HostIsolationPanel, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/cell_actions.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/cell_actions.tsx index 322568c6a53f..173520ce2d55 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/cell_actions.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/cell_actions.tsx @@ -9,7 +9,7 @@ import type { FC } from 'react'; import React, { useMemo } from 'react'; import { useDocumentDetailsContext } from '../../shared/context'; import { getSourcererScopeId } from '../../../../helpers'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { SecurityCellActionType } from '../../../../app/actions/constants'; import { CellActionsMode, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts index 16e1c5ffe8df..430eb52b0951 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts @@ -15,9 +15,9 @@ import { useDocumentDetailsContext } from '../../shared/context'; import { useInvestigationTimeEnrichment } from '../../shared/hooks/use_investigation_enrichment'; import type { RouteSpyState } from '../../../../common/utils/route/types'; import { - type GetBasicDataFromDetailsData, + type UseBasicDataFromDetailsDataResult, useBasicDataFromDetailsData, -} from '../../../../timelines/components/side_panel/event_details/helpers'; +} from '../../shared/hooks/use_basic_data_from_details_data'; import { mockContextValue } from '../../shared/mocks/mock_context'; jest.mock('../../../../timelines/containers/details'); @@ -25,7 +25,7 @@ jest.mock('../../../../sourcerer/containers'); jest.mock('../../../../common/utils/route/use_route_spy'); jest.mock('../../shared/context'); jest.mock('../../shared/hooks/use_investigation_enrichment'); -jest.mock('../../../../timelines/components/side_panel/event_details/helpers'); +jest.mock('../../shared/hooks/use_basic_data_from_details_data'); describe('useThreatIntelligenceDetails', () => { beforeEach(() => { @@ -42,7 +42,7 @@ describe('useThreatIntelligenceDetails', () => { jest .mocked(useBasicDataFromDetailsData) - .mockReturnValue({ isAlert: true } as unknown as GetBasicDataFromDetailsData); + .mockReturnValue({ isAlert: true } as unknown as UseBasicDataFromDetailsDataResult); jest.mocked(useSourcererDataView).mockReturnValue({ browserFields: {}, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.ts index 15c934718981..a7b8256b502f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.ts @@ -9,7 +9,7 @@ import { useMemo } from 'react'; import { SecurityPageName } from '@kbn/deeplinks-security'; import type { RunTimeMappings } from '../../../../../common/api/search_strategy'; import type { CtiEnrichment, EventFields } from '../../../../../common/search_strategy'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { filterDuplicateEnrichments, getEnrichmentFields, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx index b908185cd9d9..ca01ac08d66a 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx @@ -15,7 +15,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../common/lib/kibana'; import { useDocumentDetailsContext } from '../../shared/context'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { ALERT_DESCRIPTION_DETAILS_TEST_ID, ALERT_DESCRIPTION_TITLE_TEST_ID, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx index b7625075b98d..8d3b0577230a 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx @@ -15,7 +15,7 @@ import { DocumentStatus } from './status'; import { DocumentSeverity } from './severity'; import { RiskScore } from './risk_score'; import { useRefetchByScope } from '../hooks/use_refetch_by_scope'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { useDocumentDetailsContext } from '../../shared/context'; import { PreferenceFormattedDate } from '../../../../common/components/formatted_date'; import { FLYOUT_ALERT_HEADER_TITLE_TEST_ID, ALERT_SUMMARY_PANEL_TEST_ID } from './test_ids'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx index 4bed17e24b77..953a2371ffa8 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx @@ -11,7 +11,7 @@ import { EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FlyoutTitle } from '../../../shared/components/flyout_title'; import { DocumentSeverity } from './severity'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { useDocumentDetailsContext } from '../../shared/context'; import { PreferenceFormattedDate } from '../../../../common/components/formatted_date'; import { FLYOUT_EVENT_HEADER_TITLE_TEST_ID } from './test_ids'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx index 00fbd9303c33..f90d67f87f37 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx @@ -11,7 +11,7 @@ import { EuiButtonIcon, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@ import { i18n } from '@kbn/i18n'; import { NewChatByTitle } from '@kbn/elastic-assistant'; import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { useAssistant } from '../hooks/use_assistant'; import { ALERT_SUMMARY_CONVERSATION_ID, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx index 22e5b65bdade..32e170bf757d 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx @@ -12,7 +12,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiPanel, EuiTitle } from import { FormattedMessage } from '@kbn/i18n-react'; import { convertHighlightedFieldsToTableRow } from '../../shared/utils/highlighted_fields_helpers'; import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { HighlightedFieldsCell } from './highlighted_fields_cell'; import { CellActions } from './cell_actions'; import { HIGHLIGHTED_FIELDS_DETAILS_TEST_ID, HIGHLIGHTED_FIELDS_TITLE_TEST_ID } from './test_ids'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx index 7f137dc1815c..d97821428745 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx @@ -21,9 +21,11 @@ import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_f import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; import { mockContextValue } from '../../shared/mocks/mock_context'; import { useExpandSection } from '../hooks/use_expand_section'; +import { useHighlightedFields } from '../../shared/hooks/use_highlighted_fields'; jest.mock('../../../../detection_engine/rule_management/logic/use_rule_with_fallback'); jest.mock('../hooks/use_expand_section'); +jest.mock('../../shared/hooks/use_highlighted_fields'); const panelContextValue = { ...mockContextValue, @@ -65,6 +67,7 @@ describe('', () => { it('should render the component expanded if value is true in local storage', () => { (useExpandSection as jest.Mock).mockReturnValue(true); + (useHighlightedFields as jest.Mock).mockReturnValue([]); const { getByTestId } = renderInvestigationSection(); expect(getByTestId(INVESTIGATION_SECTION_CONTENT_TEST_ID)).toBeVisible(); @@ -72,6 +75,7 @@ describe('', () => { it('should render investigation guide and highlighted fields when document is signal', () => { (useExpandSection as jest.Mock).mockReturnValue(true); + (useHighlightedFields as jest.Mock).mockReturnValue([]); const { getByTestId } = renderInvestigationSection(); expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toBeInTheDocument(); @@ -80,6 +84,7 @@ describe('', () => { it('should not render investigation guide when document is not signal', () => { (useExpandSection as jest.Mock).mockReturnValue(true); + (useHighlightedFields as jest.Mock).mockReturnValue([]); const mockGetFieldsData = (field: string) => { switch (field) { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx index 1ce7e9ed5394..c4b0e6e26a82 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx @@ -20,7 +20,7 @@ import { REASON_DETAILS_TEST_ID, REASON_TITLE_TEST_ID, } from './test_ids'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { useDocumentDetailsContext } from '../../shared/context'; export const ALERT_REASON_BANNER = { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/header.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/header.test.tsx index 8130174ebbda..97a15becda64 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/header.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/header.test.tsx @@ -11,12 +11,12 @@ import { renderReactTestingLibraryWithI18n as render } from '@kbn/test-jest-help import { PanelHeader } from './header'; import { allThreeTabs } from './hooks/use_tabs'; import { GuidedOnboardingTourStep } from '../../../common/components/guided_onboarding_tour/tour_step'; -import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../shared/hooks/use_basic_data_from_details_data'; jest.mock('../shared/context', () => ({ useDocumentDetailsContext: jest.fn().mockReturnValue({ dataFormattedForFieldBrowser: [] }), })); -jest.mock('../../../timelines/components/side_panel/event_details/helpers', () => ({ +jest.mock('../shared/hooks/use_basic_data_from_details_data', () => ({ useBasicDataFromDetailsData: jest.fn(), })); jest.mock('../../../common/components/guided_onboarding_tour/tour_step', () => ({ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/header.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/header.tsx index b327fccea3be..3bf4e1a74125 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/header.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/header.tsx @@ -16,7 +16,7 @@ import { FlyoutHeaderTabs } from '../../shared/components/flyout_header_tabs'; import { AlertHeaderTitle } from './components/alert_header_title'; import { EventHeaderTitle } from './components/event_header_title'; import { useDocumentDetailsContext } from '../shared/context'; -import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../shared/hooks/use_basic_data_from_details_data'; import { AlertsCasesTourSteps, getTourAnchor, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx index 13b6dc506d39..3cecf2b0acfe 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx @@ -81,7 +81,11 @@ describe('useAssistant', () => { expect(await getPromptContext()).toEqual({ '@timestamp': ['2023-01-01T01:01:01.000Z'], + _id: ['_id'], + _index: ['index'], + 'agent.id': ['agent.id'], 'event.category': ['registry'], + 'host.name': ['host-name'], 'kibana.alert.ancestors.id': ['ancestors-id'], 'kibana.alert.rule.description': ['rule-description'], 'kibana.alert.rule.indices': ['rule-indices'], @@ -89,8 +93,10 @@ describe('useAssistant', () => { 'kibana.alert.rule.parameters.index': ['rule-parameters-index'], 'kibana.alert.rule.type': ['query'], 'kibana.alert.rule.uuid': ['rule-uuid'], + 'kibana.alert.url': ['alert-url'], 'kibana.alert.workflow_status': ['open'], 'process.entity_id': ['process-entity_id'], + 'user.name': ['user-name'], }); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_threat_intelligence.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_threat_intelligence.ts index ac59f6c802a8..7f9f26cb89da 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_threat_intelligence.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_threat_intelligence.ts @@ -9,7 +9,7 @@ import { useMemo } from 'react'; import { groupBy } from 'lodash'; import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import type { CtiEnrichment } from '../../../../../common/search_strategy'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { filterDuplicateEnrichments, getEnrichmentFields, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx index 28985471a394..64e10766ad21 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx @@ -45,7 +45,7 @@ describe('useSessionPreview', () => { expect(hookResult.result.current).toEqual({ index: 'kibana.alert.ancestors.index', - investigatedAlertId: 'id', + investigatedAlertId: '_id', jumpToCursor: '2023-01-01T00:00:00.000Z', jumpToEntityId: 'process.entity_id', sessionEntityId: 'process.entry_leader.entity_id', @@ -79,8 +79,8 @@ describe('useSessionPreview', () => { }); expect(hookResult.result.current).toEqual({ - index: '.some-index', - investigatedAlertId: 'id', + index: 'index', + investigatedAlertId: '_id', jumpToCursor: '2023-01-01T00:00:00.000Z', jumpToEntityId: 'process.entity_id', sessionEntityId: 'process.entry_leader.entity_id', diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts index ec53a6a2e00f..95c79e6815bf 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts @@ -9,7 +9,7 @@ import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types'; import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; import { getField } from '../../shared/utils'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; export interface UseSessionPreviewParams { /** diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx index 1197e39ad86c..bdfae953303c 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx @@ -13,7 +13,7 @@ import { useEventDetails } from './hooks/use_event_details'; import { FlyoutError } from '../../shared/components/flyout_error'; import { FlyoutLoading } from '../../shared/components/flyout_loading'; import type { SearchHit } from '../../../../common/search_strategy'; -import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from './hooks/use_basic_data_from_details_data'; import type { DocumentDetailsProps } from './types'; import type { GetFieldsData } from '../../../common/hooks/use_get_fields_data'; import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.test.tsx new file mode 100644 index 000000000000..b4cd7c35824a --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.test.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 { renderHook } from '@testing-library/react-hooks'; +import { useBasicDataFromDetailsData } from './use_basic_data_from_details_data'; +import { mockDataFormattedForFieldBrowser } from '../mocks/mock_data_formatted_for_field_browser'; + +describe('useBasicDataFromDetailsData', () => { + it('should return all empty properties', () => { + const hookResult = renderHook(() => useBasicDataFromDetailsData(null)); + + expect(hookResult.result.current.agentId).toEqual(''); + expect(hookResult.result.current.alertId).toEqual(''); + expect(hookResult.result.current.alertUrl).toEqual(''); + expect(hookResult.result.current.data).toEqual(null); + expect(hookResult.result.current.hostName).toEqual(''); + expect(hookResult.result.current.indexName).toEqual(''); + expect(hookResult.result.current.isAlert).toEqual(false); + expect(hookResult.result.current.ruleDescription).toEqual(''); + expect(hookResult.result.current.ruleId).toEqual(''); + expect(hookResult.result.current.ruleName).toEqual(''); + expect(hookResult.result.current.timestamp).toEqual(''); + expect(hookResult.result.current.userName).toEqual(''); + }); + + it('should return all properties', () => { + const hookResult = renderHook(() => + useBasicDataFromDetailsData(mockDataFormattedForFieldBrowser) + ); + + expect(hookResult.result.current.agentId).toEqual('agent.id'); + expect(hookResult.result.current.alertId).toEqual('_id'); + expect(hookResult.result.current.alertUrl).toEqual('alert-url'); + expect(hookResult.result.current.data).toEqual(mockDataFormattedForFieldBrowser); + expect(hookResult.result.current.hostName).toEqual('host-name'); + expect(hookResult.result.current.indexName).toEqual('index'); + expect(hookResult.result.current.isAlert).toEqual(true); + expect(hookResult.result.current.ruleDescription).toEqual('rule-description'); + expect(hookResult.result.current.ruleId).toEqual('rule-uuid'); + expect(hookResult.result.current.ruleName).toEqual('rule-name'); + expect(hookResult.result.current.timestamp).toEqual('2023-01-01T01:01:01.000Z'); + expect(hookResult.result.current.userName).toEqual('user-name'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.tsx new file mode 100644 index 000000000000..9eee4eb29b59 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.tsx @@ -0,0 +1,120 @@ +/* + * 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 { some } from 'lodash/fp'; +import { useMemo } from 'react'; +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import { getAlertDetailsFieldValue } from '../../../../common/lib/endpoint/utils/get_event_details_field_values'; + +export interface UseBasicDataFromDetailsDataResult { + agentId: string; + alertId: string; + alertUrl: string; + data: TimelineEventsDetailsItem[] | null; + hostName: string; + indexName: string; + isAlert: boolean; + ruleDescription: string; + ruleId: string; + ruleName: string; + timestamp: string; + userName: string; +} + +export const useBasicDataFromDetailsData = ( + data: TimelineEventsDetailsItem[] | null +): UseBasicDataFromDetailsDataResult => { + const agentId = useMemo( + () => getAlertDetailsFieldValue({ category: 'agent', field: 'agent.id' }, data), + [data] + ); + + const alertId = useMemo( + () => getAlertDetailsFieldValue({ category: '_id', field: '_id' }, data), + [data] + ); + + const alertUrl = useMemo( + () => getAlertDetailsFieldValue({ category: 'kibana', field: 'kibana.alert.url' }, data), + [data] + ); + + const hostName = useMemo( + () => getAlertDetailsFieldValue({ category: 'host', field: 'host.name' }, data), + [data] + ); + + const indexName = useMemo( + () => getAlertDetailsFieldValue({ category: '_index', field: '_index' }, data), + [data] + ); + + const isAlert = some({ category: 'kibana', field: 'kibana.alert.rule.uuid' }, data); + + const ruleDescription = useMemo( + () => + getAlertDetailsFieldValue( + { category: 'kibana', field: 'kibana.alert.rule.description' }, + data + ), + [data] + ); + + const ruleId = useMemo( + () => + isAlert + ? getAlertDetailsFieldValue({ category: 'kibana', field: 'kibana.alert.rule.uuid' }, data) + : getAlertDetailsFieldValue({ category: 'signal', field: 'signal.rule.id' }, data), + [isAlert, data] + ); + + const ruleName = useMemo( + () => getAlertDetailsFieldValue({ category: 'kibana', field: 'kibana.alert.rule.name' }, data), + [data] + ); + + const timestamp = useMemo( + () => getAlertDetailsFieldValue({ category: 'base', field: '@timestamp' }, data), + [data] + ); + + const userName = useMemo( + () => getAlertDetailsFieldValue({ category: 'user', field: 'user.name' }, data), + [data] + ); + + return useMemo( + () => ({ + agentId, + alertId, + alertUrl, + data, + hostName, + indexName, + isAlert, + ruleDescription, + ruleId, + ruleName, + timestamp, + userName, + }), + [ + agentId, + alertId, + alertUrl, + data, + hostName, + indexName, + isAlert, + ruleDescription, + ruleId, + ruleName, + timestamp, + userName, + ] + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx index 2c56b3d67d82..6eb8c242c79f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx @@ -23,9 +23,15 @@ describe('useHighlightedFields', () => { it('should return data', () => { const hookResult = renderHook(() => useHighlightedFields({ dataFormattedForFieldBrowser })); expect(hookResult.result.current).toEqual({ + 'host.name': { + values: ['host-name'], + }, 'kibana.alert.rule.type': { values: ['query'], }, + 'user.name': { + values: ['user-name'], + }, }); }); @@ -63,9 +69,15 @@ describe('useHighlightedFields', () => { ); expect(hookResult.result.current).toEqual({ + 'host.name': { + values: ['host-name'], + }, 'kibana.alert.rule.type': { values: ['query'], }, + 'user.name': { + values: ['user-name'], + }, }); }); @@ -93,11 +105,17 @@ describe('useHighlightedFields', () => { ); expect(hookResult.result.current).toEqual({ + 'host.name': { + values: ['host-name'], + }, 'kibana.alert.rule.type': { values: ['query'], }, 'agent.id': { - values: ['deb35a20-70f8-458e-a64a-c9e6f7575893'], + values: ['agent.id'], + }, + 'user.name': { + values: ['user-name'], }, }); }); @@ -121,9 +139,15 @@ describe('useHighlightedFields', () => { ); expect(hookResult.result.current).toEqual({ + 'host.name': { + values: ['host-name'], + }, 'kibana.alert.rule.type': { values: ['query'], }, + 'user.name': { + values: ['user-name'], + }, }); }); @@ -143,9 +167,15 @@ describe('useHighlightedFields', () => { ); expect(hookResult.result.current).toEqual({ + 'host.name': { + values: ['host-name'], + }, 'kibana.alert.rule.type': { values: ['query'], }, + 'user.name': { + values: ['user-name'], + }, }); }); @@ -175,12 +205,18 @@ describe('useHighlightedFields', () => { ); expect(hookResult.result.current).toEqual({ + 'host.name': { + values: ['host-name'], + }, 'kibana.alert.rule.type': { values: ['query'], }, [agentIdField]: { values: ['deb35a20-70f8-458e-a64a-c9e6f7575893'], }, + 'user.name': { + values: ['user-name'], + }, }); } ); @@ -209,12 +245,18 @@ describe('useHighlightedFields', () => { ); expect(hookResult.result.current).toEqual({ + 'host.name': { + values: ['host-name'], + }, 'kibana.alert.rule.type': { values: ['query'], }, 'device.id': { values: ['expectedCrowdstrikeAgentId'], }, + 'user.name': { + values: ['user-name'], + }, }); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.test.ts index aef75b40f199..f7e3a40e60c4 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.test.ts @@ -11,12 +11,12 @@ import type { UseInvestigationGuideParams, UseInvestigationGuideResult, } from './use_investigation_guide'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from './use_basic_data_from_details_data'; import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback'; import { mockDataFormattedForFieldBrowser } from '../mocks/mock_data_formatted_for_field_browser'; import { useInvestigationGuide } from './use_investigation_guide'; -jest.mock('../../../../timelines/components/side_panel/event_details/helpers'); +jest.mock('./use_basic_data_from_details_data'); jest.mock('../../../../detection_engine/rule_management/logic/use_rule_with_fallback'); const dataFormattedForFieldBrowser = mockDataFormattedForFieldBrowser; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.ts index 306cdbbb5d63..1de48b26f25b 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.ts @@ -6,8 +6,8 @@ */ import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; -import type { GetBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import type { UseBasicDataFromDetailsDataResult } from './use_basic_data_from_details_data'; +import { useBasicDataFromDetailsData } from './use_basic_data_from_details_data'; import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback'; export interface UseInvestigationGuideParams { @@ -27,11 +27,11 @@ export interface UseInvestigationGuideResult { */ error: unknown; /** - * + * The basic alert fields and their value */ - basicAlertData: GetBasicDataFromDetailsData; + basicAlertData: UseBasicDataFromDetailsDataResult; /** - * + * The note from the rule */ ruleNote: string | undefined; } diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_data_formatted_for_field_browser.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_data_formatted_for_field_browser.ts index b097215b72c2..d96ac6dfe01d 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_data_formatted_for_field_browser.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_data_formatted_for_field_browser.ts @@ -16,6 +16,20 @@ export const ruleTypeField: TimelineEventsDetailsItem = { }; export const baseFields: TimelineEventsDetailsItem[] = [ + { + category: 'agent', + field: 'agent.id', + values: ['agent.id'], + originalValue: ['agent.id'], + isObjectArray: false, + }, + { + category: '_id', + field: '_id', + values: ['_id'], + originalValue: ['_id'], + isObjectArray: false, + }, { category: 'base', field: '@timestamp', @@ -65,6 +79,13 @@ export const baseFields: TimelineEventsDetailsItem[] = [ originalValue: ['rule-parameters-index'], isObjectArray: false, }, + { + category: 'kibana', + field: 'kibana.alert.url', + values: ['alert-url'], + originalValue: ['alert-url'], + isObjectArray: false, + }, { category: 'kibana', field: 'kibana.alert.rule.uuid', @@ -86,6 +107,27 @@ export const baseFields: TimelineEventsDetailsItem[] = [ originalValue: ['process-entity_id'], isObjectArray: false, }, + { + category: 'host', + field: 'host.name', + values: ['host-name'], + originalValue: ['host-name'], + isObjectArray: false, + }, + { + category: 'user', + field: 'user.name', + values: ['user-name'], + originalValue: ['user-name'], + isObjectArray: false, + }, + { + category: '_index', + field: '_index', + values: ['index'], + originalValue: ['index'], + isObjectArray: false, + }, ]; /** diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx index acfdde85b682..fc1941b6824c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx @@ -5,121 +5,8 @@ * 2.0. */ -import { some } from 'lodash/fp'; -import { useMemo } from 'react'; -import { getAlertDetailsFieldValue } from '../../../../common/lib/endpoint/utils/get_event_details_field_values'; -import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; import { DEFAULT_ALERTS_INDEX, DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants'; -export interface GetBasicDataFromDetailsData { - agentId?: string; - alertId: string; - alertUrl?: string; - data: TimelineEventsDetailsItem[] | null; - hostName: string; - indexName?: string; - isAlert: boolean; - ruleDescription: string; - ruleId: string; - ruleName: string; - timestamp: string; - userName: string; -} - -export const useBasicDataFromDetailsData = ( - data: TimelineEventsDetailsItem[] | null -): GetBasicDataFromDetailsData => { - const isAlert = some({ category: 'kibana', field: 'kibana.alert.rule.uuid' }, data); - - const ruleId = useMemo( - () => - isAlert - ? getAlertDetailsFieldValue({ category: 'kibana', field: 'kibana.alert.rule.uuid' }, data) - : getAlertDetailsFieldValue({ category: 'signal', field: 'signal.rule.id' }, data), - [isAlert, data] - ); - - const ruleName = useMemo( - () => getAlertDetailsFieldValue({ category: 'kibana', field: 'kibana.alert.rule.name' }, data), - [data] - ); - - const ruleDescription = useMemo( - () => - getAlertDetailsFieldValue( - { category: 'kibana', field: 'kibana.alert.rule.description' }, - data - ), - [data] - ); - - const alertId = useMemo( - () => getAlertDetailsFieldValue({ category: '_id', field: '_id' }, data), - [data] - ); - - const indexName = useMemo( - () => getAlertDetailsFieldValue({ category: '_index', field: '_index' }, data), - [data] - ); - - const alertUrl = useMemo( - () => getAlertDetailsFieldValue({ category: 'kibana', field: 'kibana.alert.url' }, data), - [data] - ); - - const agentId = useMemo( - () => getAlertDetailsFieldValue({ category: 'agent', field: 'agent.id' }, data), - [data] - ); - - const hostName = useMemo( - () => getAlertDetailsFieldValue({ category: 'host', field: 'host.name' }, data), - [data] - ); - - const userName = useMemo( - () => getAlertDetailsFieldValue({ category: 'user', field: 'user.name' }, data), - [data] - ); - - const timestamp = useMemo( - () => getAlertDetailsFieldValue({ category: 'base', field: '@timestamp' }, data), - [data] - ); - - return useMemo( - () => ({ - agentId, - alertId, - alertUrl, - data, - hostName, - indexName, - isAlert, - ruleDescription, - ruleId, - ruleName, - timestamp, - userName, - }), - [ - agentId, - alertId, - alertUrl, - data, - hostName, - indexName, - isAlert, - ruleDescription, - ruleId, - ruleName, - timestamp, - userName, - ] - ); -}; - /* The referenced alert _index in the flyout uses the `.internal.` such as `.internal.alerts-security.alerts-spaceId` in the alert page flyout and diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b134c6acfc58..421ccac4a3df 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2544,6 +2544,11 @@ "discover.embeddable.search.displayName": "rechercher", "discover.errorCalloutShowErrorMessage": "Afficher les détails", "discover.esqlMode.selectedColumnsCallout": "Affichage de {selectedColumnsNumber} champs sur {esqlQueryColumnsNumber}. Ajoutez-en d’autres depuis la liste des champs disponibles.", + "discover.esqlToDataViewTransitionModal.closeButtonLabel": "Basculer sans sauvegarder", + "discover.esqlToDataViewTransitionModal.dismissButtonLabel": "Ne plus afficher cet avertissement", + "discover.esqlToDataViewTransitionModal.saveButtonLabel": "Sauvegarder et basculer", + "discover.esqlToDataViewTransitionModal.title": "Votre requête sera supprimée", + "discover.esqlToDataviewTransitionModalBody": "Modifier la vue de données supprime la requête ES|QL en cours. Sauvegardez cette recherche pour ne pas perdre de travail.", "discover.fieldChooser.availableFieldsTooltip": "Champs disponibles pour l'affichage dans le tableau.", "discover.fieldChooser.discoverField.addFieldTooltip": "Ajouter le champ en tant que colonne", "discover.fieldChooser.discoverField.removeFieldTooltip": "Supprimer le champ du tableau", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 39b898482ad0..3a26962f3fb3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2544,6 +2544,11 @@ "discover.embeddable.search.displayName": "検索", "discover.errorCalloutShowErrorMessage": "詳細を表示", "discover.esqlMode.selectedColumnsCallout": "{esqlQueryColumnsNumber}フィールド中{selectedColumnsNumber}フィールドを表示中です。利用可能なフィールドリストからさらに追加します。", + "discover.esqlToDataViewTransitionModal.closeButtonLabel": "保存せずに切り替え", + "discover.esqlToDataViewTransitionModal.dismissButtonLabel": "次回以降この警告を表示しない", + "discover.esqlToDataViewTransitionModal.saveButtonLabel": "保存して切り替え", + "discover.esqlToDataViewTransitionModal.title": "クエリは削除されます", + "discover.esqlToDataviewTransitionModalBody": "データビューを切り替えると、現在のES|QLクエリが削除されます。この検索を保存すると、作業内容が失われないことが保証されます。", "discover.fieldChooser.availableFieldsTooltip": "フィールドをテーブルに表示できます。", "discover.fieldChooser.discoverField.addFieldTooltip": "フィールドを列として追加", "discover.fieldChooser.discoverField.removeFieldTooltip": "フィールドを表から削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0d6c56ae59a3..d601c699a931 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2546,6 +2546,11 @@ "discover.embeddable.search.displayName": "搜索", "discover.errorCalloutShowErrorMessage": "查看详情", "discover.esqlMode.selectedColumnsCallout": "正在显示 {selectedColumnsNumber} 个字段,共 {esqlQueryColumnsNumber} 个。从可用字段列表中添加更多字段。", + "discover.esqlToDataViewTransitionModal.closeButtonLabel": "切换而不保存", + "discover.esqlToDataViewTransitionModal.dismissButtonLabel": "不再显示此警告", + "discover.esqlToDataViewTransitionModal.saveButtonLabel": "保存并切换", + "discover.esqlToDataViewTransitionModal.title": "将移除您的查询", + "discover.esqlToDataviewTransitionModalBody": "切换数据视图会移除当前的 ES|QL 查询。保存此搜索以确保不会丢失工作。", "discover.fieldChooser.availableFieldsTooltip": "适用于在表中显示的字段。", "discover.fieldChooser.discoverField.addFieldTooltip": "将字段添加为列", "discover.fieldChooser.discoverField.removeFieldTooltip": "从表中移除字段", From 58af1e3855bc01ada20ae5ffdb923e172957874a Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 19 Aug 2024 12:12:31 +0200 Subject: [PATCH 04/25] [MX] Fix cases functional test (#190602) ## Summary Fix flaky test suit in `x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts`. Check my solution for flakiness here: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6780#_ ### 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 Co-authored-by: Elastic Machine --- x-pack/test/functional/services/cases/list.ts | 2 +- .../apps/cases/group2/attachment_framework.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/services/cases/list.ts b/x-pack/test/functional/services/cases/list.ts index 34db5e7345b3..7f4e7346d970 100644 --- a/x-pack/test/functional/services/cases/list.ts +++ b/x-pack/test/functional/services/cases/list.ts @@ -209,8 +209,8 @@ export function CasesTableServiceProvider( return; } + await testSubjects.click('options-filter-popover-button-owner'); await retry.waitFor(`filterByOwner popover opened`, async () => { - await testSubjects.click('options-filter-popover-button-owner'); return await testSubjects.exists('options-filter-popover-panel-owner'); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts index 49e8fb47db64..146e72ab3698 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts @@ -268,8 +268,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/157642 - describe.skip('Modal', () => { + describe('Modal', () => { const createdCases = new Map(); const openModal = async () => { From 1f1a3594615afceda26308abf163497114b5858f Mon Sep 17 00:00:00 2001 From: "elastic-renovate-prod[bot]" <174716857+elastic-renovate-prod[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 05:31:56 -0500 Subject: [PATCH 05/25] Update dependency elastic-apm-node to ^4.7.3 (main) (#190314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [elastic-apm-node](https://togithub.com/elastic/apm-agent-nodejs) | dependencies | patch | [`^4.7.2` -> `^4.7.3`](https://renovatebot.com/diffs/npm/elastic-apm-node/4.7.2/4.7.3) | --- ### Release Notes
elastic/apm-agent-nodejs (elastic-apm-node) ### [`v4.7.3`](https://togithub.com/elastic/apm-agent-nodejs/releases/tag/v4.7.3) [Compare Source](https://togithub.com/elastic/apm-agent-nodejs/compare/v4.7.2...v4.7.3) For more information, please see the [changelog](https://www.elastic.co/guide/en/apm/agent/nodejs/current/release-notes-4.x.html#release-notes-4.7.3). ##### Elastic APM Node.js agent layer ARNs |Region|ARN| |------|---| |af-south-1|arn:aws:lambda:af-south-1:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |ap-east-1|arn:aws:lambda:ap-east-1:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |ap-northeast-1|arn:aws:lambda:ap-northeast-1:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |ap-northeast-2|arn:aws:lambda:ap-northeast-2:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |ap-northeast-3|arn:aws:lambda:ap-northeast-3:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |ap-south-1|arn:aws:lambda:ap-south-1:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |ap-southeast-1|arn:aws:lambda:ap-southeast-1:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |ap-southeast-2|arn:aws:lambda:ap-southeast-2:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |ap-southeast-3|arn:aws:lambda:ap-southeast-3:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |ca-central-1|arn:aws:lambda:ca-central-1:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |eu-central-1|arn:aws:lambda:eu-central-1:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |eu-north-1|arn:aws:lambda:eu-north-1:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |eu-south-1|arn:aws:lambda:eu-south-1:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |eu-west-1|arn:aws:lambda:eu-west-1:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |eu-west-2|arn:aws:lambda:eu-west-2:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |eu-west-3|arn:aws:lambda:eu-west-3:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |me-south-1|arn:aws:lambda:me-south-1:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |sa-east-1|arn:aws:lambda:sa-east-1:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |us-east-1|arn:aws:lambda:us-east-1:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |us-east-2|arn:aws:lambda:us-east-2:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |us-west-1|arn:aws:lambda:us-west-1:267093732750:layer:elastic-apm-node-ver-4-7-3:1| |us-west-2|arn:aws:lambda:us-west-2:267093732750:layer:elastic-apm-node-ver-4-7-3:1|
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://togithub.com/renovatebot/renovate). Co-authored-by: elastic-renovate-prod[bot] <174716857+elastic-renovate-prod[bot]@users.noreply.github.com> Co-authored-by: Elastic Machine --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 4b2303136e00..23fcac6bc90e 100644 --- a/package.json +++ b/package.json @@ -1054,7 +1054,7 @@ "deepmerge": "^4.2.2", "del": "^6.1.0", "diff": "^5.1.0", - "elastic-apm-node": "^4.7.2", + "elastic-apm-node": "^4.7.3", "email-addresses": "^5.0.0", "eventsource-parser": "^1.1.1", "execa": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index 8d3f4a10b126..b8332a6a4c37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16589,10 +16589,10 @@ elastic-apm-node@3.46.0: traverse "^0.6.6" unicode-byte-truncate "^1.0.0" -elastic-apm-node@^4.7.2: - version "4.7.2" - resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-4.7.2.tgz#880b3df8e2266aac70f6370f916b0e66d5063455" - integrity sha512-9jsvAeHU6wztM+qUWJvgJCgdCVUI1sfg6a9quXmgkcjUJmRDJG0trfTScELZrfK5VJBQ88LVl05Q0nJW2j6TsA== +elastic-apm-node@^4.7.3: + version "4.7.3" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-4.7.3.tgz#d819a9030f7321cc858c788f60b383de85461f24" + integrity sha512-x+cQKrXSCz6JgoTFAiBpLlC85ruqZ7sAl+jAS3+DeSmc6ZXLRTwTa2ay2PCGv3DxGLZjVZ+ItzGdHTj5B7PYKg== dependencies: "@elastic/ecs-pino-format" "^1.5.0" "@opentelemetry/api" "^1.4.1" @@ -16612,7 +16612,7 @@ elastic-apm-node@^4.7.2: fast-safe-stringify "^2.0.7" fast-stream-to-buffer "^1.0.0" http-headers "^3.0.2" - import-in-the-middle "1.9.1" + import-in-the-middle "1.11.0" json-bigint "^1.0.0" lru-cache "10.2.0" measured-reporting "^1.51.1" @@ -20003,10 +20003,10 @@ import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: parent-module "^1.0.0" resolve-from "^4.0.0" -import-in-the-middle@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.9.1.tgz#83f68c0ca926709257562238e1993a1c31e01272" - integrity sha512-E+3tEOutU1MV0mxhuCwfSPNNWRkbTJ3/YyL5be+blNIbHwZc53uYHQfuIhAU77xWR0BoF2eT7cqDJ6VlU5APPg== +import-in-the-middle@1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz#a94c4925b8da18256cde3b3b7b38253e6ca5e708" + integrity sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q== dependencies: acorn "^8.8.2" acorn-import-attributes "^1.9.5" From 51df0599aa46adb27f6ba8eb0265f18536de84d4 Mon Sep 17 00:00:00 2001 From: Alex Szabo Date: Mon, 19 Aug 2024 13:57:53 +0200 Subject: [PATCH 06/25] [CI] Fix flaky anomaly detection functional test (#190673) ## Summary There are several cases where anomaly detection tests fail (find below). The error claims an unhandled promise rejection through a stale element. It could be simply a check or an operation that happens after the browser driver moves on to another test. I originally wanted to find the tests that seem to fail, but the nature of the failures (unhandled rejections) line up nicely with missed `await`s - I've also added a supposed fix, verified with 50x flaky runs. Errors: - https://buildkite.com/elastic/kibana-on-merge/builds/49005#01916999-a25b-4c59-8d91-99cf4b83981b - https://buildkite.com/elastic/kibana-on-merge/builds/49004 - https://buildkite.com/elastic/kibana-elasticsearch-snapshot-verify/builds/4373 - https://buildkite.com/elastic/kibana-on-merge/builds/49002 - ... --- .../lens_to_ml_with_wizard.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/ml/anomaly_detection_integrations/lens_to_ml_with_wizard.ts b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/lens_to_ml_with_wizard.ts index 87f4bfd36335..b17f2657d1ea 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection_integrations/lens_to_ml_with_wizard.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection_integrations/lens_to_ml_with_wizard.ts @@ -136,7 +136,7 @@ export default function ({ getService, getPageObject, getPageObjects }: FtrProvi await ml.lensVisualizations.assertNumberOfIncompatibleLensLayers(numberOfIncompatibleLayers); - ml.lensVisualizations.clickCreateJobFromLayerWithWizard(0); + await ml.lensVisualizations.clickCreateJobFromLayerWithWizard(0); await retrySwitchTab(1, 10); tabsCount++; @@ -161,7 +161,7 @@ export default function ({ getService, getPageObject, getPageObjects }: FtrProvi await ml.lensVisualizations.assertNumberOfIncompatibleLensLayers(numberOfIncompatibleLayers); - ml.lensVisualizations.clickCreateJobFromLayerWithWizard(0); + await ml.lensVisualizations.clickCreateJobFromLayerWithWizard(0); await retrySwitchTab(1, 10); tabsCount++; @@ -186,7 +186,7 @@ export default function ({ getService, getPageObject, getPageObjects }: FtrProvi await ml.lensVisualizations.assertNumberOfIncompatibleLensLayers(numberOfIncompatibleLayers); - ml.lensVisualizations.clickCreateJobFromLayerWithWizard(1); + await ml.lensVisualizations.clickCreateJobFromLayerWithWizard(1); await retrySwitchTab(1, 10); tabsCount++; @@ -215,7 +215,7 @@ export default function ({ getService, getPageObject, getPageObjects }: FtrProvi await dashboardPreparation(selectedPanelTitle); - ml.lensVisualizations.assertMLJobMenuActionDoesNotExist(selectedPanelTitle); + await ml.lensVisualizations.assertMLJobMenuActionDoesNotExist(selectedPanelTitle); }); }); } From d1c4bb33fb6af42300759269186d39348814933e Mon Sep 17 00:00:00 2001 From: Luke Gmys <11671118+lgestc@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:02:36 +0200 Subject: [PATCH 07/25] [Security Solution] Remove active patterns from Sourcerer (#190020) ## Summary Another round of field removal, this time I am replacing the `activePatterns` field with `dataView.title` based logic for active pattern retrieval. This is mostly about upgrade flow for sourcerer going from stack v7 to 8. --- .../hooks/use_threat_intelligence_details.test.ts | 1 - .../public/sourcerer/components/index.test.tsx | 8 ++++++-- .../public/sourcerer/components/index.tsx | 9 ++++++++- .../public/sourcerer/components/misc.test.tsx | 7 +++++-- .../components/sourcerer_integration.test.tsx | 1 - .../public/sourcerer/containers/mocks.ts | 1 - .../public/sourcerer/store/model.ts | 15 +-------------- 7 files changed, 20 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts index 430eb52b0951..e40cd74709cf 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts @@ -49,7 +49,6 @@ describe('useThreatIntelligenceDetails', () => { dataViewId: '', loading: false, indicesExist: true, - patternList: [], selectedPatterns: [], indexPattern: { fields: [], title: '' }, sourcererDataView: undefined, diff --git a/x-pack/plugins/security_solution/public/sourcerer/components/index.test.tsx b/x-pack/plugins/security_solution/public/sourcerer/components/index.test.tsx index 99209f44cfbe..1f39bc02c68c 100644 --- a/x-pack/plugins/security_solution/public/sourcerer/components/index.test.tsx +++ b/x-pack/plugins/security_solution/public/sourcerer/components/index.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import type { ReactWrapper } from 'enzyme'; import { mount } from 'enzyme'; -import { SourcererScopeName } from '../store/model'; +import { type SelectedDataView, SourcererScopeName } from '../store/model'; import { Sourcerer } from '.'; import { sourcererActions, sourcererModel } from '../store'; import { createMockStore, mockGlobalState, TestProviders } from '../../common/mock'; @@ -74,9 +74,13 @@ const { id, patternList, title } = mockGlobalState.sourcerer.defaultDataView; const patternListNoSignals = sortWithExcludesAtEnd( patternList.filter((p) => p !== mockGlobalState.sourcerer.signalIndexName) ); -const sourcererDataView = { +const sourcererDataView: Partial = { indicesExist: true, loading: false, + sourcererDataView: { + title: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', + }, }; describe('Sourcerer component', () => { diff --git a/x-pack/plugins/security_solution/public/sourcerer/components/index.tsx b/x-pack/plugins/security_solution/public/sourcerer/components/index.tsx index 3dcdf49391d3..0f946bd8c247 100644 --- a/x-pack/plugins/security_solution/public/sourcerer/components/index.tsx +++ b/x-pack/plugins/security_solution/public/sourcerer/components/index.tsx @@ -149,12 +149,19 @@ export const Sourcerer = React.memo(({ scope: scopeId } } }, [isDetectionsSourcerer, isTimelineSourcerer, pollForSignalIndex]); - const { activePatterns, indicesExist, loading } = useSourcererDataView(scopeId); + const { indicesExist, loading, sourcererDataView } = useSourcererDataView(scopeId); + + const activePatterns = useMemo( + () => (sourcererDataView?.title || '')?.split(',').filter(Boolean) as string[], + [sourcererDataView?.title] + ); + const [missingPatterns, setMissingPatterns] = useState( activePatterns && activePatterns.length > 0 ? sourcererMissingPatterns.filter((p) => activePatterns.includes(p)) : [] ); + useEffect(() => { if (activePatterns && activePatterns.length > 0) { setMissingPatterns(sourcererMissingPatterns.filter((p) => activePatterns.includes(p))); diff --git a/x-pack/plugins/security_solution/public/sourcerer/components/misc.test.tsx b/x-pack/plugins/security_solution/public/sourcerer/components/misc.test.tsx index 8a1d333355e4..1897acca1c6d 100644 --- a/x-pack/plugins/security_solution/public/sourcerer/components/misc.test.tsx +++ b/x-pack/plugins/security_solution/public/sourcerer/components/misc.test.tsx @@ -10,7 +10,7 @@ import type { ReactWrapper } from 'enzyme'; import { mount } from 'enzyme'; import { cloneDeep } from 'lodash'; -import { initialSourcererState, SourcererScopeName } from '../store/model'; +import { initialSourcererState, type SelectedDataView, SourcererScopeName } from '../store/model'; import { Sourcerer } from '.'; import { sourcererActions, sourcererModel } from '../store'; import { createMockStore, mockGlobalState, TestProviders } from '../../common/mock'; @@ -74,9 +74,12 @@ const { id, patternList } = mockGlobalState.sourcerer.defaultDataView; const patternListNoSignals = sortWithExcludesAtEnd( patternList.filter((p) => p !== mockGlobalState.sourcerer.signalIndexName) ); -const sourcererDataView = { +const sourcererDataView: Partial = { indicesExist: true, loading: false, + sourcererDataView: { + title: 'myFakebeat-*', + }, }; describe('No data', () => { diff --git a/x-pack/plugins/security_solution/public/sourcerer/components/sourcerer_integration.test.tsx b/x-pack/plugins/security_solution/public/sourcerer/components/sourcerer_integration.test.tsx index de4bd8b63947..d43a3a47ed26 100644 --- a/x-pack/plugins/security_solution/public/sourcerer/components/sourcerer_integration.test.tsx +++ b/x-pack/plugins/security_solution/public/sourcerer/components/sourcerer_integration.test.tsx @@ -109,7 +109,6 @@ describe('Sourcerer integration tests', () => { (useSourcererDataView as jest.Mock).mockReturnValue({ ...sourcererDataView, - activePatterns: ['myFakebeat-*'], }); jest.clearAllMocks(); }); diff --git a/x-pack/plugins/security_solution/public/sourcerer/containers/mocks.ts b/x-pack/plugins/security_solution/public/sourcerer/containers/mocks.ts index e3c30a0eb652..283f41bc8be6 100644 --- a/x-pack/plugins/security_solution/public/sourcerer/containers/mocks.ts +++ b/x-pack/plugins/security_solution/public/sourcerer/containers/mocks.ts @@ -56,5 +56,4 @@ export const mockSourcererScope: SelectedDataView = { indicesExist: true, loading: false, dataViewId: mockGlobalState.sourcerer.defaultDataView.id, - patternList: mockPatterns, }; diff --git a/x-pack/plugins/security_solution/public/sourcerer/store/model.ts b/x-pack/plugins/security_solution/public/sourcerer/store/model.ts index 6120b3c66cf7..3b3f8c56b261 100644 --- a/x-pack/plugins/security_solution/public/sourcerer/store/model.ts +++ b/x-pack/plugins/security_solution/public/sourcerer/store/model.ts @@ -97,21 +97,8 @@ export interface SelectedDataView { indicesExist: boolean; /** is an update being made to the data view */ loading: boolean; - /** - * @deprecated use sourcererDataView.title or sourcererDataView.matchedIndices - * all active & inactive patterns from SourcererDataView['title'] - */ - patternList: string[]; - /** - * @deprecated use sourcererDataView.title or sourcererDataView.matchedIndices - * all selected patterns from SourcererScope['selectedPatterns'] */ + /* all selected patterns from SourcererScope['selectedPatterns'] */ selectedPatterns: SourcererScope['selectedPatterns']; - /** - * @deprecated use sourcererDataView.title or sourcererDataView.matchedIndices - * active patterns when dataViewId == null - */ - activePatterns?: string[]; - /** * Easier to add this additional data rather than * try to extend the SelectedDataView type from DataView. From f8e873f87d15014a6f5364482e72ef3ff67e46ad Mon Sep 17 00:00:00 2001 From: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:27:06 +0200 Subject: [PATCH 08/25] [TSVB] Visualization blows up when invalid color is passed (#190658) https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6784 ## Summary Fixes https://github.com/elastic/kibana/issues/190657 Fixes https://github.com/elastic/kibana/issues/182136 Co-authored-by: Elastic Machine --- .../components/color_picker.test.tsx | 80 +++++++++++-------- .../application/components/color_picker.tsx | 8 +- .../visualize/group5/_tsvb_time_series.ts | 4 +- 3 files changed, 56 insertions(+), 36 deletions(-) diff --git a/src/plugins/vis_types/timeseries/public/application/components/color_picker.test.tsx b/src/plugins/vis_types/timeseries/public/application/components/color_picker.test.tsx index 3c32275ea5ee..061922840419 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/color_picker.test.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/color_picker.test.tsx @@ -8,60 +8,76 @@ import React from 'react'; import { ColorPicker, ColorPickerProps } from './color_picker'; -import { mount } from 'enzyme'; -import { ReactWrapper } from 'enzyme'; -import { EuiColorPicker, EuiIconTip } from '@elastic/eui'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { fireEvent, render, screen } from '@testing-library/react'; describe('ColorPicker', () => { + const onChange = jest.fn(); const defaultProps: ColorPickerProps = { name: 'color', value: null, - onChange: jest.fn(), + onChange, disableTrash: true, }; - let component: ReactWrapper; + + const renderColorPicker = (props?: Partial) => + render(); + + afterEach(() => { + jest.clearAllMocks(); + }); it('should render the EuiColorPicker', () => { - component = mount(); - expect(component.find(EuiColorPicker).length).toBe(1); + renderColorPicker(); + expect(screen.getByTestId('tvbColorPicker')).toBeInTheDocument(); }); it('should not render the clear button', () => { - component = mount(); - expect(findTestSubject(component, 'tvbColorPickerClear').length).toBe(0); + renderColorPicker(); + expect(screen.queryByTestId('tvbColorPickerClear')).toBeNull(); }); - it('should render the correct value to the input text if the prop value is hex', () => { - const props = { ...defaultProps, value: '#68BC00' }; - component = mount(); - findTestSubject(component, 'tvbColorPicker').find('button').simulate('click'); - const input = findTestSubject(component, 'euiColorPickerInput_top'); - expect(input.props().value).toBe('#68BC00'); + it('should render incorrect value to the input text but not call onChange prop', () => { + renderColorPicker({ value: '#68BC00' }); + fireEvent.click(screen.getByRole('button')); + fireEvent.change(screen.getAllByTestId('euiColorPickerInput_top')[0], { + target: { value: 'INVALID' }, + }); + expect(onChange).not.toHaveBeenCalled(); + expect(screen.getAllByTestId('euiColorPickerInput_top')[0]).toHaveValue('INVALID'); }); - - it('should render the correct value to the input text if the prop value is rgba', () => { - const props = { ...defaultProps, value: 'rgba(85,66,177,1)' }; - component = mount(); - findTestSubject(component, 'tvbColorPicker').find('button').simulate('click'); - const input = findTestSubject(component, 'euiColorPickerInput_top'); - expect(input.props().value).toBe('85,66,177,1'); + it('should render correct value to the input text and call onChange prop', () => { + renderColorPicker({ value: '#68BC00' }); + fireEvent.click(screen.getByRole('button')); + fireEvent.change(screen.getAllByTestId('euiColorPickerInput_top')[0], { + target: { value: '#FFF' }, + }); + expect(onChange).toHaveBeenCalled(); + expect(screen.getAllByTestId('euiColorPickerInput_top')[0]).toHaveValue('#FFF'); }); it('should render the correct aria label to the color swatch button', () => { - const props = { ...defaultProps, value: 'rgba(85,66,177,0.59)' }; - component = mount(); - const button = findTestSubject(component, 'tvbColorPicker').find('button'); - expect(button.prop('aria-label')).toBe('Color picker (rgba(85,66,177,0.59)), not accessible'); + renderColorPicker({ value: 'rgba(85,66,177,0.59)' }); + expect( + screen.getByLabelText('Color picker (rgba(85,66,177,0.59)), not accessible') + ).toBeInTheDocument(); }); it('should call clear function if the disableTrash prop is false', () => { - const props = { ...defaultProps, disableTrash: false, value: 'rgba(85,66,177,1)' }; - component = mount(); + const { container } = renderColorPicker({ disableTrash: false, value: 'rgba(85,66,177,1)' }); + fireEvent.click(screen.getByTestId('tvbColorPickerClear')); + expect(onChange).toHaveBeenCalled(); + expect(container.querySelector('[data-euiicon-type="cross"]')).toBeInTheDocument(); + }); - findTestSubject(component, 'tvbColorPickerClear').simulate('click'); + it('should render the correct value to the input text if the prop value is hex', () => { + renderColorPicker({ value: '#68BC00' }); + fireEvent.click(screen.getByRole('button')); + expect(screen.getAllByTestId('euiColorPickerInput_top')[0]).toHaveValue('#68BC00'); + }); - expect(component.find(EuiIconTip).length).toBe(1); - expect(defaultProps.onChange).toHaveBeenCalled(); + it('should render the correct value to the input text if the prop value is rgba', () => { + renderColorPicker({ value: 'rgba(85,66,177,1)' }); + fireEvent.click(screen.getByRole('button')); + expect(screen.getAllByTestId('euiColorPickerInput_top')[0]).toHaveValue('85,66,177,1'); }); }); diff --git a/src/plugins/vis_types/timeseries/public/application/components/color_picker.tsx b/src/plugins/vis_types/timeseries/public/application/components/color_picker.tsx index a44134dfa919..474ce9541279 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/color_picker.tsx +++ b/src/plugins/vis_types/timeseries/public/application/components/color_picker.tsx @@ -41,8 +41,14 @@ export function ColorPicker({ name, value, disableTrash = false, onChange }: Col const { euiTheme } = useEuiTheme(); - const handleColorChange: EuiColorPickerProps['onChange'] = (text: string, { rgba, hex }) => { + const handleColorChange: EuiColorPickerProps['onChange'] = ( + text: string, + { rgba, hex, isValid } + ) => { setColor(text); + if (!isValid) { + return; + } onChange({ [name]: hex ? `rgba(${rgba.join(',')})` : '' }); }; diff --git a/test/functional/apps/visualize/group5/_tsvb_time_series.ts b/test/functional/apps/visualize/group5/_tsvb_time_series.ts index 55c59e38f359..5e471b3ebb82 100644 --- a/test/functional/apps/visualize/group5/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/group5/_tsvb_time_series.ts @@ -145,8 +145,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(actualCountMin).to.be('3 hours'); }); - // FLAKY: https://github.com/elastic/kibana/issues/182136 - describe.skip('Dark mode', () => { + describe('Dark mode', () => { before(async () => { await kibanaServer.uiSettings.update({ 'theme:darkMode': true, @@ -156,7 +155,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it(`viz should have light class when background color is white`, async () => { await visualBuilder.clickPanelOptions('timeSeries'); await visualBuilder.setBackgroundColor('#FFFFFF'); - await retry.try(async () => { expect(await visualBuilder.checkTimeSeriesIsLight()).to.be(true); }); From 965b0a675d537f3a83dcf7eaa6153a33af1b62f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Mon, 19 Aug 2024 14:46:15 +0200 Subject: [PATCH 09/25] Wait between instances, but not after the last one (#190679) --- .../migrations/group2/multiple_kibana_nodes.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kibana_nodes.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kibana_nodes.test.ts index c02f9bc47d36..8d6f83736d01 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kibana_nodes.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kibana_nodes.test.ts @@ -182,7 +182,8 @@ describe('migration v2', () => { errors.push(err.message); }) ); - if (i < instances.length - 1) { + if (i < instances.length - 2) { + // We wait between instances, but not after the last one await delay(delayInSec * 1000); } } From 0299a7a3bc0b77619bffb387c13afa592251c060 Mon Sep 17 00:00:00 2001 From: Umberto Pepato Date: Mon, 19 Aug 2024 14:50:50 +0200 Subject: [PATCH 10/25] [ResponseOps][Alerts] Add null-value bucket detection to alerts group aggregations endpoint (#190305) ## Summary - Adds null-value bucket detection to server-side alerts aggregations and marks those groups with a `--` key and `isNullGroup = true`. - Improves alerts grouping types with default aggregations. - Improves documentation ## To verify 1. Temporarily merge [#189958](https://github.com/elastic/kibana/pull/189958) into this branch 2. Create a rule that fires alerts in Observability > Alerts (i.e. Custom Threshold, ES Query, ...) 3. Once you start to see some alerts in the Alerts page, toggle the grouped alerts view using the dropdown at the top-right of the table (`Group alerts by: ...`), selecting a custom field that doesn't have a value in alert documents (to find one, open the alert flyout and look at the fields table) 4. Check that the group based on the empty field shows `--` as a title 5. Check that the alerts table in the expanded group panel is filtered correctly ### References Refs [#189958](https://github.com/elastic/kibana/pull/189958) ### Checklist Delete any items that are not applicable to this PR. - [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 --- packages/kbn-alerts-grouping/index.ts | 6 ++- .../src/components/alerts_grouping.tsx | 43 ++++++++++++++----- .../src/components/alerts_grouping_level.tsx | 12 +++--- packages/kbn-alerts-grouping/src/types.ts | 27 +++++++++++- ...use_get_alerts_group_aggregations_query.ts | 2 + .../server/alert_data_client/alerts_client.ts | 42 ++++++++++++++---- .../get_alerts_group_aggregations.test.ts | 40 ++++++++++++++--- 7 files changed, 141 insertions(+), 31 deletions(-) diff --git a/packages/kbn-alerts-grouping/index.ts b/packages/kbn-alerts-grouping/index.ts index e9e2476dde7a..f124e5759619 100644 --- a/packages/kbn-alerts-grouping/index.ts +++ b/packages/kbn-alerts-grouping/index.ts @@ -7,5 +7,9 @@ */ export { AlertsGrouping } from './src/components/alerts_grouping'; -export { type AlertsGroupingProps } from './src/types'; +export { + type AlertsGroupingProps, + type BaseAlertsGroupAggregations, + type AlertsGroupAggregationBucket, +} from './src/types'; export { useAlertsGroupingState } from './src/contexts/alerts_grouping_context'; diff --git a/packages/kbn-alerts-grouping/src/components/alerts_grouping.tsx b/packages/kbn-alerts-grouping/src/components/alerts_grouping.tsx index f17d79466837..5db1ef5a5d0f 100644 --- a/packages/kbn-alerts-grouping/src/components/alerts_grouping.tsx +++ b/packages/kbn-alerts-grouping/src/components/alerts_grouping.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { useAlertsDataView } from '@kbn/alerts-ui-shared/src/common/hooks/use_alerts_data_view'; import useLocalStorage from 'react-use/lib/useLocalStorage'; import { AlertsGroupingLevel, AlertsGroupingLevelProps } from './alerts_grouping_level'; -import { AlertsGroupingProps } from '../types'; +import type { AlertsGroupingProps, BaseAlertsGroupAggregations } from '../types'; import { AlertsGroupingContextProvider, useAlertsGroupingState, @@ -40,7 +40,10 @@ const NextLevel = ({ parentGroupingFilter, groupingFilters, getLevel, -}: Pick & { +}: Pick< + AlertsGroupingLevelProps, + 'children' | 'parentGroupingFilter' +> & { level: number; selectedGroups: string[]; groupingFilters: Filter[]; @@ -56,7 +59,9 @@ const NextLevel = ({ return children(nextGroupingFilters)!; }; -const AlertsGroupingInternal = (props: AlertsGroupingProps) => { +const AlertsGroupingInternal = ( + props: AlertsGroupingProps +) => { const { groupingId, services, @@ -230,6 +235,8 @@ const AlertsGroupingInternal = (props: AlertsGroupingProps) => { return getLevel(0, selectedGroups[0]); }; +const typedMemo: (c: T) => T = memo; + /** * A coordinator component to show multiple alert tables grouped by one or more fields * @@ -243,7 +250,7 @@ const AlertsGroupingInternal = (props: AlertsGroupingProps) => { * * * return ( - * * featureIds={[...]} * globalQuery={{ query: ..., language: 'kql' }} * globalFilters={...} @@ -274,11 +281,25 @@ const AlertsGroupingInternal = (props: AlertsGroupingProps) => { * * ); * ``` + * + * To define your aggregations result type, extend the `BaseAlertsGroupAggregations` type: + * + * ```ts + * import { BaseAlertsGroupAggregations } from '@kbn/alerts-grouping'; + * + * interface YourAggregationsType extends BaseAlertsGroupAggregations { + * // Your custom aggregations here + * } + * ``` + * + * Check {@link useGetAlertsGroupAggregationsQuery} for more info on alerts aggregations. */ -export const AlertsGrouping = memo((props: AlertsGroupingProps) => { - return ( - - - - ); -}); +export const AlertsGrouping = typedMemo( + (props: AlertsGroupingProps) => { + return ( + + + + ); + } +); diff --git a/packages/kbn-alerts-grouping/src/components/alerts_grouping_level.tsx b/packages/kbn-alerts-grouping/src/components/alerts_grouping_level.tsx index e4511e8dea77..a82818215cbf 100644 --- a/packages/kbn-alerts-grouping/src/components/alerts_grouping_level.tsx +++ b/packages/kbn-alerts-grouping/src/components/alerts_grouping_level.tsx @@ -18,10 +18,11 @@ import { useGetAlertsGroupAggregationsQuery, UseGetAlertsGroupAggregationsQueryProps, } from '@kbn/alerts-ui-shared'; -import { AlertsGroupingProps } from '../types'; +import { AlertsGroupingProps, BaseAlertsGroupAggregations } from '../types'; -export interface AlertsGroupingLevelProps = {}> - extends AlertsGroupingProps { +export interface AlertsGroupingLevelProps< + T extends BaseAlertsGroupAggregations = BaseAlertsGroupAggregations +> extends AlertsGroupingProps { getGrouping: ( props: Omit, 'groupSelector' | 'pagination'> ) => ReactElement; @@ -40,8 +41,9 @@ const DEFAULT_FILTERS: Filter[] = []; /** * Renders an alerts grouping level */ -export const AlertsGroupingLevel = memo( - = {}>({ +const typedMemo: (c: T) => T = memo; +export const AlertsGroupingLevel = typedMemo( + ({ featureIds, defaultFilters = DEFAULT_FILTERS, from, diff --git a/packages/kbn-alerts-grouping/src/types.ts b/packages/kbn-alerts-grouping/src/types.ts index 8d226bb74e71..835941e8db95 100644 --- a/packages/kbn-alerts-grouping/src/types.ts +++ b/packages/kbn-alerts-grouping/src/types.ts @@ -29,7 +29,9 @@ export interface AlertsGroupingState { [groupingId: string]: GroupModel; } -export interface AlertsGroupingProps = {}> { +export interface AlertsGroupingProps< + T extends BaseAlertsGroupAggregations = BaseAlertsGroupAggregations +> { /** * The leaf component that will be rendered in the grouping panels */ @@ -96,3 +98,26 @@ export interface AlertsGroupingProps = {}> { http: HttpSetup; }; } + +export interface AlertsGroupAggregationBucket { + key: string; + doc_count: number; + isNullGroup?: boolean; + unitsCount?: { + value: number; + }; +} + +export interface BaseAlertsGroupAggregations { + groupByFields: { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: AlertsGroupAggregationBucket[]; + }; + groupsCount: { + value: number; + }; + unitsCount: { + value: number; + }; +} diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_get_alerts_group_aggregations_query.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_get_alerts_group_aggregations_query.ts index eab5b9ac510f..e9e24b7a20d5 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_get_alerts_group_aggregations_query.ts +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_get_alerts_group_aggregations_query.ts @@ -45,6 +45,8 @@ export interface UseGetAlertsGroupAggregationsQueryProps { * * The provided `aggregations` are applied within `groupByFields`. Here the `groupByField` runtime * field can be used to perform grouping-based aggregations. + * `groupByField` buckets computed over a field with a null/absent value are marked with the + * `isNullGroup` flag set to true and their key is set to the `--` string. * * Applies alerting RBAC through featureIds. */ diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index 4dac5ff41390..e03a403037aa 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -27,12 +27,18 @@ import { } from '@kbn/rule-data-utils'; import { + AggregateName, + AggregationsAggregate, + AggregationsMultiBucketAggregateBase, InlineScript, MappingRuntimeFields, QueryDslQueryContainer, SortCombinations, -} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { RuleTypeParams, PluginStartContract as AlertingStart } from '@kbn/alerting-plugin/server'; +} from '@elastic/elasticsearch/lib/api/types'; +import type { + RuleTypeParams, + PluginStartContract as AlertingStart, +} from '@kbn/alerting-plugin/server'; import { ReadOperations, AlertingAuthorization, @@ -279,7 +285,7 @@ export class AlertsClient { /** * Searches alerts by id or query and audits the results */ - private async searchAlerts({ + private async searchAlerts>({ id, query, aggs, @@ -335,7 +341,7 @@ export class AlertsClient { }; } - const result = await this.esClient.search({ + const result = await this.esClient.search({ index: index ?? '.alerts-*', ignore_unavailable: true, body: queryBody, @@ -975,7 +981,10 @@ export class AlertsClient { } } - public async find({ + public async find< + Params extends RuleTypeParams = never, + TAggregations = Record + >({ aggs, featureIds, index, @@ -1007,7 +1016,7 @@ export class AlertsClient { } } - const alertsSearchResponse = await this.searchAlerts({ + const alertsSearchResponse = await this.searchAlerts({ query, aggs, _source, @@ -1036,7 +1045,7 @@ export class AlertsClient { /** * Performs a `find` query to extract aggregations on alert groups */ - public getGroupAggregations({ + public async getGroupAggregations({ featureIds, groupByField, aggregations, @@ -1086,7 +1095,10 @@ export class AlertsClient { `The number of documents is too high. Paginating through more than ${MAX_PAGINATED_ALERTS} documents is not possible.` ); } - return this.find({ + const searchResult = await this.find< + never, + { groupByFields: AggregationsMultiBucketAggregateBase<{ key: string }> } + >({ featureIds, aggs: { groupByFields: { @@ -1139,6 +1151,20 @@ export class AlertsClient { size: 0, _source: false, }); + // Replace artificial uuid values with '--' in null-value buckets and mark them with `isNullGroup = true` + const groupsAggregation = searchResult.aggregations?.groupByFields; + if (groupsAggregation) { + const buckets = Array.isArray(groupsAggregation?.buckets) + ? groupsAggregation.buckets + : Object.values(groupsAggregation?.buckets ?? {}); + buckets.forEach((bucket) => { + if (bucket.key === uniqueValue) { + bucket.key = '--'; + (bucket as { isNullGroup?: boolean }).isNullGroup = true; + } + }); + } + return searchResult; } public async getAuthorizedAlertsIndices(featureIds: string[]): Promise { diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get_alerts_group_aggregations.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get_alerts_group_aggregations.test.ts index 8aedf715ff66..af10edf37238 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get_alerts_group_aggregations.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get_alerts_group_aggregations.test.ts @@ -72,7 +72,7 @@ beforeEach(() => { describe('getGroupAggregations()', () => { test('calls find() with the correct params', async () => { const alertsClient = new AlertsClient(alertsClientParams); - alertsClient.find = jest.fn(); + alertsClient.find = jest.fn().mockResolvedValue({ aggregations: {} }); const featureIds = [AlertConsumers.STACK_ALERTS]; const groupByField = 'kibana.alert.rule.name'; @@ -141,27 +141,57 @@ describe('getGroupAggregations()', () => { }); }); + test('replaces the key of null-value buckets and marks them with the `isNullGroup` flag', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + alertsClient.find = jest.fn().mockResolvedValue({ + aggregations: { + groupByFields: { + buckets: [ + { + key: 'unique-value', + doc_count: 1, + }, + ], + }, + }, + }); + + const result = await alertsClient.getGroupAggregations({ + featureIds: [AlertConsumers.STACK_ALERTS], + groupByField: 'kibana.alert.rule.name', + aggregations: {}, + filters: [], + pageIndex: 0, + pageSize: DEFAULT_ALERTS_GROUP_BY_FIELD_SIZE, + }); + + const firstBucket = (result.aggregations as any).groupByFields.buckets[0]; + + expect(firstBucket.isNullGroup).toBe(true); + expect(firstBucket.key).toEqual('--'); + }); + test('rejects with invalid pagination options', async () => { const alertsClient = new AlertsClient(alertsClientParams); - expect(() => + await expect(() => alertsClient.getGroupAggregations({ featureIds: ['apm', 'infrastructure', 'logs', 'observability', 'slo', 'uptime'], groupByField: 'kibana.alert.rule.name', pageIndex: 101, pageSize: 50, }) - ).toThrowErrorMatchingInlineSnapshot( + ).rejects.toThrowErrorMatchingInlineSnapshot( `"The provided pageIndex value is too high. The maximum allowed pageIndex value is 100."` ); - expect(() => + await expect(() => alertsClient.getGroupAggregations({ featureIds: ['apm', 'infrastructure', 'logs', 'observability', 'slo', 'uptime'], groupByField: 'kibana.alert.rule.name', pageIndex: 10, pageSize: 5000, }) - ).toThrowErrorMatchingInlineSnapshot( + ).rejects.toThrowErrorMatchingInlineSnapshot( `"The number of documents is too high. Paginating through more than 10000 documents is not possible."` ); }); From 15657536ed82d945c2dfc1251b3558e547311c26 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Mon, 19 Aug 2024 15:09:47 +0200 Subject: [PATCH 11/25] [Fleet] Create task that periodically unenrolls inactive agents (#189861) Closes https://github.com/elastic/kibana/issues/179399 ## Summary Create a new periodic task that unenrolls inactive agents based on `unenroll_timeout` set on agent policies In the agent policy settings there is now a new section: ![Screenshot 2024-08-06 at 12 31 37](https://github.com/user-attachments/assets/f66164c5-3eff-442d-91bc-367387cefe3d) ### Testing - Create a policy with `unenroll_timeout` set to any value - Enroll many agents to a policy and make them inactive - you can use Horde or the script in `fleet/scripts/create_agents' that can directly create inactive agents - Leave the local env running for at least 10 minutes - You should see logs that indicate that the task ran successfully and remove the inactive agents ![Screenshot 2024-08-06 at 12 14 13](https://github.com/user-attachments/assets/573f32fb-eedb-4bee-918c-f26fedec9e0b) Note that the executed unenroll action is also visible in the UI: ![Screenshot 2024-08-06 at 12 19 52](https://github.com/user-attachments/assets/942932ac-70dd-4d77-bf47-20007ac54748) - If there are no agent policies with `unenroll_timeout` set or there are no inactive agents on those policies, you should see logs like these: ![Screenshot 2024-08-06 at 12 13 49](https://github.com/user-attachments/assets/8868c228-fd09-4ecf-ad02-e07a94812638) ### Checklist - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [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 --------- Co-authored-by: Elastic Machine --- .../agent_policy_advanced_fields/index.tsx | 20 +- .../agent_activity_flyout/activity_item.tsx | 6 +- .../agent_activity_flyout/helpers.tsx | 15 +- x-pack/plugins/fleet/server/mocks/index.ts | 1 + x-pack/plugins/fleet/server/plugin.ts | 10 + .../fleet/server/services/agent_policy.ts | 4 - .../unenroll_inactive_agents_task.test.ts | 181 ++++++++++++++++ .../tasks/unenroll_inactive_agents_task.ts | 204 ++++++++++++++++++ .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../check_registered_task_types.ts | 1 + 12 files changed, 415 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.test.ts create mode 100644 x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx index 185c694a5c84..28d14b62a575 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx @@ -22,7 +22,6 @@ import { EuiText, EuiFlexGroup, EuiFlexItem, - EuiBetaBadge, EuiBadge, EuiSwitch, } from '@elastic/eui'; @@ -796,29 +795,14 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent =

-   - - -

} description={ } > diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/activity_item.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/activity_item.tsx index 96b48948320b..ee83c49744d3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/activity_item.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/activity_item.tsx @@ -55,7 +55,7 @@ export const ActivityItem: React.FunctionComponent<{ ? action.nbAgentsAck : action.nbAgentsAck + ' of ' + action.nbAgentsActioned, agents: action.nbAgentsActioned === 1 ? 'agent' : 'agents', - completedText: getAction(action.type).completedText, + completedText: getAction(action.type, action.actionId).completedText, offlineText: action.status === 'ROLLOUT_PASSED' && action.nbAgentsActioned - action.nbAgentsAck > 0 ? `, ${ @@ -175,7 +175,7 @@ export const ActivityItem: React.FunctionComponent<{ id="xpack.fleet.agentActivityFlyout.cancelledTitle" defaultMessage="Agent {cancelledText} cancelled" values={{ - cancelledText: getAction(action.type).cancelledText, + cancelledText: getAction(action.type, action.actionId).cancelledText, }} /> @@ -201,7 +201,7 @@ export const ActivityItem: React.FunctionComponent<{ id="xpack.fleet.agentActivityFlyout.expiredTitle" defaultMessage="Agent {expiredText} expired" values={{ - expiredText: getAction(action.type).cancelledText, + expiredText: getAction(action.type, action.actionId).cancelledText, }} /> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/helpers.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/helpers.tsx index a3c9d5807fae..82fc266a04bc 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/helpers.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_activity_flyout/helpers.tsx @@ -31,6 +31,11 @@ const actionNames: { completedText: 'force unenrolled', cancelledText: 'force unenrollment', }, + AUTOMATIC_FORCE_UNENROLL: { + inProgressText: 'Automatic unenrolling', + completedText: 'automatically unenrolled', + cancelledText: 'automatic unenrollment', + }, UPDATE_TAGS: { inProgressText: 'Updating tags of', completedText: 'updated tags', @@ -60,7 +65,13 @@ const actionNames: { ACTION: { inProgressText: 'Actioning', completedText: 'actioned', cancelledText: 'action' }, }; -export const getAction = (type?: string) => actionNames[type ?? 'ACTION'] ?? actionNames.ACTION; +export const getAction = (type?: string, actionId?: string) => { + // handling a special case of force unenrollment coming from an automatic task + // we know what kind of action is from the actionId prefix + if (actionId?.includes('UnenrollInactiveAgentsTask-')) + return actionNames.AUTOMATICAL_FORCE_UNENROLL; + return actionNames[type ?? 'ACTION'] ?? actionNames.ACTION; +}; export const inProgressTitle = (action: ActionStatus) => ( ( ? action.nbAgentsActioned : action.nbAgentsActioned - action.nbAgentsAck + ' of ' + action.nbAgentsActioned, agents: action.nbAgentsActioned === 1 ? 'agent' : 'agents', - inProgressText: getAction(action.type).inProgressText, + inProgressText: getAction(action.type, action.actionId).inProgressText, reassignText: action.type === 'POLICY_REASSIGN' && action.newPolicyId ? `to ${action.newPolicyId}` : '', upgradeText: action.type === 'UPGRADE' ? `to version ${action.version}` : '', diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 6a91add910fb..7b11654f8def 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -128,6 +128,7 @@ export const createAppContextStartContractMock = ( }, } : {}), + unenrollInactiveAgentsTask: {} as any, }; }; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index d05ef1de3364..043b02ca93d7 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -118,6 +118,7 @@ import type { PackagePolicyService } from './services/package_policy_service'; import { PackagePolicyServiceImpl } from './services/package_policy'; import { registerFleetUsageLogger, startFleetUsageLogger } from './services/fleet_usage_logger'; import { CheckDeletedFilesTask } from './tasks/check_deleted_files_task'; +import { UnenrollInactiveAgentsTask } from './tasks/unenroll_inactive_agents_task'; import { UninstallTokenService, type UninstallTokenServiceInterface, @@ -178,6 +179,7 @@ export interface FleetAppContext { messageSigningService: MessageSigningServiceInterface; auditLogger?: AuditLogger; uninstallTokenService: UninstallTokenServiceInterface; + unenrollInactiveAgentsTask: UnenrollInactiveAgentsTask; } export type FleetSetupContract = void; @@ -266,6 +268,7 @@ export class FleetPlugin private fleetUsageSender?: FleetUsageSender; private checkDeletedFilesTask?: CheckDeletedFilesTask; private fleetMetricsTask?: FleetMetricsTask; + private unenrollInactiveAgentsTask?: UnenrollInactiveAgentsTask; private agentService?: AgentService; private packageService?: PackageService; @@ -599,6 +602,11 @@ export class FleetPlugin taskManager: deps.taskManager, logFactory: this.initializerContext.logger, }); + this.unenrollInactiveAgentsTask = new UnenrollInactiveAgentsTask({ + core, + taskManager: deps.taskManager, + logFactory: this.initializerContext.logger, + }); // Register fields metadata extractor registerIntegrationFieldsExtractor({ core, fieldsMetadata: deps.fieldsMetadata }); @@ -644,12 +652,14 @@ export class FleetPlugin bulkActionsResolver: this.bulkActionsResolver!, messageSigningService, uninstallTokenService, + unenrollInactiveAgentsTask: this.unenrollInactiveAgentsTask!, }); licenseService.start(plugins.licensing.license$); this.telemetryEventsSender.start(plugins.telemetry, core).catch(() => {}); this.bulkActionsResolver?.start(plugins.taskManager).catch(() => {}); this.fleetUsageSender?.start(plugins.taskManager).catch(() => {}); this.checkDeletedFilesTask?.start({ taskManager: plugins.taskManager }).catch(() => {}); + this.unenrollInactiveAgentsTask?.start({ taskManager: plugins.taskManager }).catch(() => {}); startFleetUsageLogger(plugins.taskManager).catch(() => {}); this.fleetMetricsTask ?.start(plugins.taskManager, core.elasticsearch.client.asInternalUser) diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index b2ba41b78586..250cd867ee87 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -1245,10 +1245,6 @@ class AgentPolicyService { default_fleet_server: policy.is_default_fleet_server === true, }; - if (policy.unenroll_timeout) { - fleetServerPolicy.unenroll_timeout = policy.unenroll_timeout; - } - acc.push(fleetServerPolicy); return acc; }, [] as FleetServerPolicy[]); diff --git a/x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.test.ts b/x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.test.ts new file mode 100644 index 000000000000..dd1121a90462 --- /dev/null +++ b/x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.test.ts @@ -0,0 +1,181 @@ +/* + * 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 { coreMock } from '@kbn/core/server/mocks'; +import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; +import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; +import { TaskStatus } from '@kbn/task-manager-plugin/server'; +import { getDeleteTaskRunResult } from '@kbn/task-manager-plugin/server/task'; +import type { CoreSetup } from '@kbn/core/server'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; + +import { agentPolicyService } from '../services'; +import { createAgentPolicyMock } from '../../common/mocks'; +import { createAppContextStartContractMock } from '../mocks'; +import { getAgentsByKuery } from '../services/agents'; + +import { appContextService } from '../services'; + +import { unenrollBatch } from '../services/agents/unenroll_action_runner'; + +import type { AgentPolicy } from '../types'; + +import { UnenrollInactiveAgentsTask, TYPE, VERSION } from './unenroll_inactive_agents_task'; + +jest.mock('../services'); +jest.mock('../services/agents'); +jest.mock('../services/agents/unenroll_action_runner'); + +const MOCK_TASK_INSTANCE = { + id: `${TYPE}:${VERSION}`, + runAt: new Date(), + attempts: 0, + ownerId: '', + status: TaskStatus.Running, + startedAt: new Date(), + scheduledAt: new Date(), + retryAt: new Date(), + params: {}, + state: {}, + taskType: TYPE, +}; + +const mockAgentPolicyService = agentPolicyService as jest.Mocked; +const mockedGetAgentsByKuery = getAgentsByKuery as jest.MockedFunction; + +describe('UnenrollInactiveAgentsTask', () => { + const { createSetup: coreSetupMock } = coreMock; + const { createSetup: tmSetupMock, createStart: tmStartMock } = taskManagerMock; + + let mockContract: ReturnType; + let mockTask: UnenrollInactiveAgentsTask; + let mockCore: CoreSetup; + let mockTaskManagerSetup: jest.Mocked; + const mockedUnenrollBatch = jest.mocked(unenrollBatch); + + const agents = [ + { + id: 'agent-1', + policy_id: 'agent-policy-2', + status: 'inactive', + }, + { + id: 'agent-2', + policy_id: 'agent-policy-1', + status: 'inactive', + }, + { + id: 'agent-3', + policy_id: 'agent-policy-1', + status: 'active', + }, + ]; + + const getMockAgentPolicyFetchAllAgentPolicies = (items: AgentPolicy[]) => + jest.fn().mockResolvedValue( + jest.fn(async function* () { + yield items; + })() + ); + + beforeEach(() => { + mockContract = createAppContextStartContractMock(); + appContextService.start(mockContract); + mockCore = coreSetupMock(); + mockTaskManagerSetup = tmSetupMock(); + mockTask = new UnenrollInactiveAgentsTask({ + core: mockCore, + taskManager: mockTaskManagerSetup, + logFactory: loggingSystemMock.create(), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Task lifecycle', () => { + it('Should create task', () => { + expect(mockTask).toBeInstanceOf(UnenrollInactiveAgentsTask); + }); + + it('Should register task', () => { + expect(mockTaskManagerSetup.registerTaskDefinitions).toHaveBeenCalled(); + }); + + it('Should schedule task', async () => { + const mockTaskManagerStart = tmStartMock(); + await mockTask.start({ taskManager: mockTaskManagerStart }); + expect(mockTaskManagerStart.ensureScheduled).toHaveBeenCalled(); + }); + }); + + describe('Task logic', () => { + const runTask = async (taskInstance = MOCK_TASK_INSTANCE) => { + const mockTaskManagerStart = tmStartMock(); + await mockTask.start({ taskManager: mockTaskManagerStart }); + const createTaskRunner = + mockTaskManagerSetup.registerTaskDefinitions.mock.calls[0][0][TYPE].createTaskRunner; + const taskRunner = createTaskRunner({ taskInstance }); + return taskRunner.run(); + }; + + beforeEach(() => { + mockAgentPolicyService.fetchAllAgentPolicies = getMockAgentPolicyFetchAllAgentPolicies([ + createAgentPolicyMock({ unenroll_timeout: 3000 }), + createAgentPolicyMock({ id: 'agent-policy-2', unenroll_timeout: 1000 }), + ]); + + mockedGetAgentsByKuery.mockResolvedValue({ + agents, + } as any); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should unenroll eligible agents', async () => { + mockedUnenrollBatch.mockResolvedValueOnce({ actionId: 'actionid-01' }); + await runTask(); + expect(mockedUnenrollBatch).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + agents, + { + force: true, + revoke: true, + actionId: expect.stringContaining('UnenrollInactiveAgentsTask-'), + } + ); + }); + + it('Should not run if task is outdated', async () => { + const result = await runTask({ ...MOCK_TASK_INSTANCE, id: 'old-id' }); + + expect(mockedUnenrollBatch).not.toHaveBeenCalled(); + expect(result).toEqual(getDeleteTaskRunResult()); + }); + + it('Should exit if there are no agents policies with unenroll_timeout set', async () => { + mockAgentPolicyService.list.mockResolvedValue({ + items: [], + total: 0, + page: 1, + perPage: 1, + }); + expect(mockedUnenrollBatch).not.toHaveBeenCalled(); + }); + + it('Should exit if there are no eligible agents to unenroll', async () => { + mockedGetAgentsByKuery.mockResolvedValue({ + agents: [], + } as any); + expect(mockedUnenrollBatch).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.ts b/x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.ts new file mode 100644 index 000000000000..d56c10cc61a2 --- /dev/null +++ b/x-pack/plugins/fleet/server/tasks/unenroll_inactive_agents_task.ts @@ -0,0 +1,204 @@ +/* + * 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 { SavedObjectsClient } from '@kbn/core/server'; +import { v4 as uuidv4 } from 'uuid'; +import type { + CoreSetup, + ElasticsearchClient, + Logger, + SavedObjectsClientContract, +} from '@kbn/core/server'; +import type { + ConcreteTaskInstance, + TaskManagerSetupContract, + TaskManagerStartContract, +} from '@kbn/task-manager-plugin/server'; +import { getDeleteTaskRunResult } from '@kbn/task-manager-plugin/server/task'; +import type { LoggerFactory } from '@kbn/core/server'; +import { errors } from '@elastic/elasticsearch'; + +import { AGENTS_PREFIX, AGENT_POLICY_SAVED_OBJECT_TYPE } from '../constants'; +import { getAgentsByKuery } from '../services/agents'; +import { unenrollBatch } from '../services/agents/unenroll_action_runner'; +import { agentPolicyService, auditLoggingService } from '../services'; + +export const TYPE = 'fleet:unenroll-inactive-agents-task'; +export const VERSION = '1.0.0'; +const TITLE = 'Fleet Unenroll Inactive Agent Task'; +const SCOPE = ['fleet']; +const INTERVAL = '10m'; +const TIMEOUT = '1m'; +const UNENROLLMENT_BATCHSIZE = 1000; +const POLICIES_BATCHSIZE = 500; + +interface UnenrollInactiveAgentsTaskSetupContract { + core: CoreSetup; + taskManager: TaskManagerSetupContract; + logFactory: LoggerFactory; +} + +interface UnenrollInactiveAgentsTaskStartContract { + taskManager: TaskManagerStartContract; +} + +export class UnenrollInactiveAgentsTask { + private logger: Logger; + private wasStarted: boolean = false; + private abortController = new AbortController(); + + constructor(setupContract: UnenrollInactiveAgentsTaskSetupContract) { + const { core, taskManager, logFactory } = setupContract; + this.logger = logFactory.get(this.taskId); + + taskManager.registerTaskDefinitions({ + [TYPE]: { + title: TITLE, + timeout: TIMEOUT, + createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => { + return { + run: async () => { + return this.runTask(taskInstance, core); + }, + cancel: async () => { + this.abortController.abort('Task timed out'); + }, + }; + }, + }, + }); + } + + public start = async ({ taskManager }: UnenrollInactiveAgentsTaskStartContract) => { + if (!taskManager) { + this.logger.error('[UnenrollInactiveAgentsTask] Missing required service during start'); + return; + } + + this.wasStarted = true; + this.logger.info(`[UnenrollInactiveAgentsTask] Started with interval of [${INTERVAL}]`); + + try { + await taskManager.ensureScheduled({ + id: this.taskId, + taskType: TYPE, + scope: SCOPE, + schedule: { + interval: INTERVAL, + }, + state: {}, + params: { version: VERSION }, + }); + } catch (e) { + this.logger.error(`Error scheduling task UnenrollInactiveAgentsTask, error: ${e.message}`, e); + } + }; + + private get taskId(): string { + return `${TYPE}:${VERSION}`; + } + + private endRun(msg: string = '') { + this.logger.info(`[UnenrollInactiveAgentsTask] runTask ended${msg ? ': ' + msg : ''}`); + } + + public async unenrollInactiveAgents( + esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract + ) { + this.logger.debug( + `[UnenrollInactiveAgentsTask] Fetching agent policies with unenroll_timeout > 0` + ); + // find all agent policies that are not managed and having unenroll_timeout > 0 + // limit the search to POLICIES_BATCHSIZE at a time and loop until there are no agent policies left + const policiesKuery = `${AGENT_POLICY_SAVED_OBJECT_TYPE}.is_managed: false AND ${AGENT_POLICY_SAVED_OBJECT_TYPE}.unenroll_timeout > 0`; + let agentCounter = 0; + + const agentPolicyFetcher = await agentPolicyService.fetchAllAgentPolicies(soClient, { + kuery: policiesKuery, + perPage: POLICIES_BATCHSIZE, + }); + for await (const agentPolicyPageResults of agentPolicyFetcher) { + this.logger.debug( + `[UnenrollInactiveAgentsTask] Found "${agentPolicyPageResults.length}" agent policies with unenroll_timeout > 0` + ); + if (!agentPolicyPageResults.length) { + this.endRun('Found no policies to process'); + return; + } + + // find inactive agents enrolled on above policies + // limit batch size to UNENROLLMENT_BATCHSIZE to avoid scale issues + const kuery = `(${AGENTS_PREFIX}.policy_id:${agentPolicyPageResults + .map((policy) => `"${policy.id}"`) + .join(' or ')}) and ${AGENTS_PREFIX}.status: inactive`; + const res = await getAgentsByKuery(esClient, soClient, { + kuery, + showInactive: true, + page: 1, + perPage: UNENROLLMENT_BATCHSIZE, + }); + if (!res.agents.length) { + this.endRun('No inactive agents to unenroll'); + return; + } + agentCounter += res.agents.length; + if (agentCounter >= UNENROLLMENT_BATCHSIZE) { + this.endRun('Reached the maximum amount of agents to unenroll, exiting.'); + return; + } + this.logger.debug( + `[UnenrollInactiveAgentsTask] Found "${res.agents.length}" inactive agents to unenroll. Attempting unenrollment` + ); + const unenrolledBatch = await unenrollBatch(soClient, esClient, res.agents, { + revoke: true, + force: true, + actionId: `UnenrollInactiveAgentsTask-${uuidv4()}`, + }); + auditLoggingService.writeCustomAuditLog({ + message: `Recurrent unenrollment of ${agentCounter} inactive agents due to unenroll_timeout option set on agent policy. Fleet action [id=${unenrolledBatch.actionId}]`, + }); + this.logger.debug( + `[UnenrollInactiveAgentsTask] Executed unenrollment of ${agentCounter} inactive agents with actionId: ${unenrolledBatch.actionId}` + ); + } + } + + public runTask = async (taskInstance: ConcreteTaskInstance, core: CoreSetup) => { + if (!this.wasStarted) { + this.logger.debug('[UnenrollInactiveAgentsTask] runTask Aborted. Task not started yet'); + return; + } + // Check that this task is current + if (taskInstance.id !== this.taskId) { + this.logger.debug( + `[UnenrollInactiveAgentsTask] Outdated task version: Got [${taskInstance.id}] from task instance. Current version is [${this.taskId}]` + ); + return getDeleteTaskRunResult(); + } + + this.logger.info(`[runTask()] started`); + + const [coreStart] = await core.getStartServices(); + const esClient = coreStart.elasticsearch.client.asInternalUser; + const soClient = new SavedObjectsClient(coreStart.savedObjects.createInternalRepository()); + + try { + await this.unenrollInactiveAgents(esClient, soClient); + + this.endRun('success'); + } catch (err) { + if (err instanceof errors.RequestAbortedError) { + this.logger.warn(`[UnenrollInactiveAgentsTask] request aborted due to timeout: ${err}`); + this.endRun(); + return; + } + this.logger.error(`[UnenrollInactiveAgentsTask] error: ${err}`); + this.endRun('error'); + } + }; +} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 421ccac4a3df..af2ab4bc10df 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -19627,10 +19627,8 @@ "xpack.fleet.agentPolicyForm.tamperingSwitchLabel": "Empêcher la falsification des agents", "xpack.fleet.agentPolicyForm.tamperingSwitchLabel.disabledWarning": "L'intégration Elastic Defend est nécessaire pour activer cette fonctionnalité", "xpack.fleet.agentPolicyForm.tamperingUninstallLink": "Obtenir la commande de désinstallation", - "xpack.fleet.agentPolicyForm.unenrollmentTimeoutDeprecatedLabel": "Déclassé", "xpack.fleet.agentPolicyForm.unenrollmentTimeoutDescription": "Délai d'expiration facultatif en secondes. Si une valeur est indiquée et que la version du serveur Fleet est inférieure à 8.7.0, un agent est automatiquement désenregistré après une période d'inactivité équivalente à ce délai.", "xpack.fleet.agentPolicyForm.unenrollmentTimeoutLabel": "Délai d'expiration pour le désenregistrement", - "xpack.fleet.agentPolicyForm.unenrollmentTimeoutTooltip": "Ce paramètre est déclassé et sera retiré dans une prochaine version. Envisagez d'utiliser le délai d'inactivité à la place", "xpack.fleet.agentPolicyForm.unenrollTimeoutMinValueErrorMessage": "Le délai de désenregistrement doit être supérieur à zéro.", "xpack.fleet.agentPolicyList.actionsColumnTitle": "Actions", "xpack.fleet.agentPolicyList.addButton": "Créer une stratégie d'agent", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3a26962f3fb3..602e8ea6b08a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19615,10 +19615,8 @@ "xpack.fleet.agentPolicyForm.tamperingSwitchLabel": "エージェントの改ざんを防止", "xpack.fleet.agentPolicyForm.tamperingSwitchLabel.disabledWarning": "この機能を有効にするには、Elastic Defend統合が必要です。", "xpack.fleet.agentPolicyForm.tamperingUninstallLink": "アンインストールコマンドを取得", - "xpack.fleet.agentPolicyForm.unenrollmentTimeoutDeprecatedLabel": "非推奨", "xpack.fleet.agentPolicyForm.unenrollmentTimeoutDescription": "任意のタイムアウト(秒)。指定され、Fleetサーバーのバージョンが8.7.0より前の場合、この期間が経過した後、エージェントは自動的に登録解除されます。", "xpack.fleet.agentPolicyForm.unenrollmentTimeoutLabel": "登録解除タイムアウト", - "xpack.fleet.agentPolicyForm.unenrollmentTimeoutTooltip": "この設定はサポートが終了し、今後のリリースでは削除されます。代わりに、非アクティブタイムアウトの使用を検討してください。", "xpack.fleet.agentPolicyForm.unenrollTimeoutMinValueErrorMessage": "登録解除タイムアウトは0よりも大きい値でなければなりません。", "xpack.fleet.agentPolicyList.actionsColumnTitle": "アクション", "xpack.fleet.agentPolicyList.addButton": "エージェントポリシーを作成", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d601c699a931..21e15ecd10f5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19641,10 +19641,8 @@ "xpack.fleet.agentPolicyForm.tamperingSwitchLabel": "防止篡改代理", "xpack.fleet.agentPolicyForm.tamperingSwitchLabel.disabledWarning": "需要 Elastic Defend 集成才能启用此功能", "xpack.fleet.agentPolicyForm.tamperingUninstallLink": "获取卸载命令", - "xpack.fleet.agentPolicyForm.unenrollmentTimeoutDeprecatedLabel": "(已过时)", "xpack.fleet.agentPolicyForm.unenrollmentTimeoutDescription": "可选超时(秒)。若提供,且 Fleet 服务器的版本低于 8.7.0,代理断开连接此段时间后,将自动注销。", "xpack.fleet.agentPolicyForm.unenrollmentTimeoutLabel": "注销超时", - "xpack.fleet.agentPolicyForm.unenrollmentTimeoutTooltip": "此设置已过时,将在未来版本中移除。考虑改用非活动超时", "xpack.fleet.agentPolicyForm.unenrollTimeoutMinValueErrorMessage": "取消注册超时必须大于零。", "xpack.fleet.agentPolicyList.actionsColumnTitle": "操作", "xpack.fleet.agentPolicyList.addButton": "创建代理策略", diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index 576f08d890e5..810db7295a79 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -140,6 +140,7 @@ export default function ({ getService }: FtrProviderContext) { 'fleet:check-deleted-files-task', 'fleet:reassign_action:retry', 'fleet:request_diagnostics:retry', + 'fleet:unenroll-inactive-agents-task', 'fleet:unenroll_action:retry', 'fleet:update_agent_tags:retry', 'fleet:upgrade_action:retry', From e7aabcdfaeec769991b0a38f2bd97d6287c22876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?= Date: Mon, 19 Aug 2024 16:05:13 +0200 Subject: [PATCH 12/25] [EDR Workflows] Improve event filters related cy tests (#190610) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary reducing potential flakyness in cypress tests handling event filters, by applying same change as #189961: enter text instead of selecting from dropdown > [!note] > revert da247c571d7c5741c618501b0614facd939e8323 before merge - done ✅ flaky runner: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6782 ✅ --- .../cypress/fixtures/artifacts_page.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts b/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts index 47f09f65dd09..97a6807a59f0 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/fixtures/artifacts_page.ts @@ -188,14 +188,11 @@ export const getArtifactsListTestsData = (): ArtifactsFixtureType[] => [ selector: 'eventFilters-form-description-input', value: 'This is the event filter description', }, - { - type: 'click', - selector: 'fieldAutocompleteComboBox', - }, + { type: 'input', - customSelector: '[data-test-subj="fieldAutocompleteComboBox"] input', - value: '@timestamp{downArrow}{enter}', + selector: 'fieldAutocompleteComboBox', + value: '@timestamp', }, { type: 'click', @@ -239,12 +236,9 @@ export const getArtifactsListTestsData = (): ArtifactsFixtureType[] => [ value: 'This is the event filter description edited', }, { - type: 'click', + type: 'input', selector: 'fieldAutocompleteComboBox', - }, - { - type: 'click', - customSelector: 'button[title="agent.name"]', + value: '{selectAll}agent.name', }, { type: 'input', From cf58ef9e516bc3146adee23fedd193364c3255f0 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Mon, 19 Aug 2024 16:06:54 +0200 Subject: [PATCH 13/25] [OneDiscover][UnifiedDocViewer] Add dedicated column for Pinning/Unpinning rows (#190344) - Closes https://github.com/elastic/kibana/issues/188413 ## Summary This PR adds a dedicated column for pinning/unpinning fields inside DocViewer. ![Aug-13-2024 15-06-25](https://github.com/user-attachments/assets/93496cdd-e730-4ee6-8597-c78d7bffe07f) ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [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 - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../src/components/field_name/field_name.tsx | 22 +-- .../table_cell_actions.test.tsx.snap | 128 +----------------- .../doc_viewer_table/get_pin_control.test.tsx | 63 +++++++++ .../doc_viewer_table/get_pin_control.tsx | 86 ++++++++++++ .../components/doc_viewer_table/table.scss | 20 ++- .../components/doc_viewer_table/table.tsx | 6 + .../doc_viewer_table/table_cell.tsx | 3 +- .../doc_viewer_table/table_cell_actions.tsx | 35 ----- .../apps/discover/group3/_doc_viewer.ts | 42 +++++- test/functional/services/data_grid.ts | 18 +++ .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 13 files changed, 236 insertions(+), 190 deletions(-) create mode 100644 src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.test.tsx create mode 100644 src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.tsx diff --git a/packages/kbn-unified-doc-viewer/src/components/field_name/field_name.tsx b/packages/kbn-unified-doc-viewer/src/components/field_name/field_name.tsx index d7f380195947..4c57fa6cc0dc 100644 --- a/packages/kbn-unified-doc-viewer/src/components/field_name/field_name.tsx +++ b/packages/kbn-unified-doc-viewer/src/components/field_name/field_name.tsx @@ -8,14 +8,7 @@ import React from 'react'; import './field_name.scss'; -import { - EuiBadge, - EuiFlexGroup, - EuiFlexItem, - EuiToolTip, - EuiHighlight, - EuiIcon, -} from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiHighlight } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { FieldIcon, FieldIconProps } from '@kbn/react-field'; @@ -30,7 +23,6 @@ interface Props { fieldIconProps?: Omit; scripted?: boolean; highlight?: string; - isPinned?: boolean; } export function FieldName({ @@ -40,7 +32,6 @@ export function FieldName({ fieldIconProps, scripted = false, highlight = '', - isPinned = false, }: Props) { const typeName = getFieldTypeName(fieldType); const displayName = @@ -63,17 +54,6 @@ export function FieldName({ - {isPinned && ( - - - - )}
diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap index bbc8ee91569a..aecef4797f9b 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/__snapshots__/table_cell_actions.test.tsx.snap @@ -43,95 +43,10 @@ Array [ } } />, - , ] `; -exports[`TableActions getFieldCellActions should render correctly for undefined functions 2`] = ` -Array [ - , -] -`; +exports[`TableActions getFieldCellActions should render correctly for undefined functions 2`] = `Array []`; exports[`TableActions getFieldCellActions should render the panels correctly for defined onFilter function 1`] = ` Array [ @@ -217,47 +132,6 @@ Array [ } } />, - , ] `; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.test.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.test.tsx new file mode 100644 index 000000000000..74282a52b86c --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { DataViewField } from '@kbn/data-views-plugin/common'; +import { TableRow } from './table_cell_actions'; +import { getPinColumnControl } from './get_pin_control'; +import { EuiDataGridCellValueElementProps } from '@elastic/eui/src/components/datagrid/data_grid_types'; + +describe('getPinControl', () => { + const rows: TableRow[] = [ + { + action: { + onFilter: jest.fn(), + flattenedField: 'flattenedField', + onToggleColumn: jest.fn(), + }, + field: { + pinned: true, + onTogglePinned: jest.fn(), + field: 'message', + fieldMapping: new DataViewField({ + type: 'keyword', + name: 'message', + searchable: true, + aggregatable: true, + }), + fieldType: 'keyword', + displayName: 'message', + scripted: false, + }, + value: { + ignored: undefined, + formattedValue: 'test', + }, + }, + ]; + + it('should render correctly', () => { + const control = getPinColumnControl({ rows }); + const Cell = control.rowCellRender as React.FC; + render( + + ); + + screen.getByTestId('unifiedDocViewer_pinControlButton_message').click(); + + expect(rows[0].field.onTogglePinned).toHaveBeenCalledWith('message'); + }); +}); diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.tsx new file mode 100644 index 000000000000..0a2c45611dcd --- /dev/null +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/get_pin_control.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiButtonIcon, + EuiDataGridControlColumn, + EuiScreenReaderOnly, + EuiToolTip, + useEuiTheme, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import type { TableRow } from './table_cell_actions'; + +interface PinControlCellProps { + row: TableRow; +} + +const PinControlCell: React.FC = React.memo(({ row }) => { + const { euiTheme } = useEuiTheme(); + + const fieldName = row.field.field; + const isPinned = row.field.pinned; + const label = isPinned + ? i18n.translate('unifiedDocViewer.docViews.table.unpinFieldLabel', { + defaultMessage: 'Unpin field', + }) + : i18n.translate('unifiedDocViewer.docViews.table.pinFieldLabel', { + defaultMessage: 'Pin field', + }); + + return ( +
+ + { + row.field.onTogglePinned(fieldName); + }} + /> + +
+ ); +}); + +export const getPinColumnControl = ({ rows }: { rows: TableRow[] }): EuiDataGridControlColumn => { + return { + id: 'pin_field', + width: 32, + headerCellRender: () => ( + + + {i18n.translate('unifiedDocViewer.fieldsTable.pinControlColumnHeader', { + defaultMessage: 'Pin field column', + })} + + + ), + rowCellRender: ({ rowIndex }) => { + const row = rows[rowIndex]; + if (!row) { + return null; + } + return ; + }, + }; +}; diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.scss b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.scss index 91022cc47faf..330cf364ae55 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.scss +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.scss @@ -80,8 +80,26 @@ background-color: tintOrShade($euiColorLightShade, 50%, 0); } - & .euiDataGridRowCell--firstColumn .euiDataGridRowCell__content { + & [data-gridcell-column-id='name'] .euiDataGridRowCell__content { padding-top: 0; padding-bottom: 0; } + + & [data-gridcell-column-id='pin_field'] .euiDataGridRowCell__content { + padding: $euiSizeXS / 2 0 0 $euiSizeXS; + } + + .kbnDocViewer__fieldsGrid__pinAction { + opacity: 0; + } + + & [data-gridcell-column-id='pin_field']:focus-within { + .kbnDocViewer__fieldsGrid__pinAction { + opacity: 1; + } + } + + .euiDataGridRow:hover .kbnDocViewer__fieldsGrid__pinAction { + opacity: 1; + } } diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx index 64659877910a..008149966c49 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table.tsx @@ -53,6 +53,7 @@ import { } from '../doc_viewer_source/get_height'; import { TableFilters, TableFiltersProps, useTableFilters } from './table_filters'; import { TableCell } from './table_cell'; +import { getPinColumnControl } from './get_pin_control'; export type FieldRecord = TableRow; @@ -295,6 +296,10 @@ export const DocViewerTable = ({ const rows = useMemo(() => [...pinnedItems, ...restItems], [pinnedItems, restItems]); + const leadingControlColumns = useMemo(() => { + return [getPinColumnControl({ rows })]; + }, [rows]); + const { curPageIndex, pageSize, totalPages, changePageIndex, changePageSize } = usePager({ initialPageSize: getPageSize(storage), totalItems: rows.length, @@ -492,6 +497,7 @@ export const DocViewerTable = ({ renderCellValue={renderCellValue} renderCellPopover={renderCellPopover} pagination={pagination} + leadingControlColumns={leadingControlColumns} /> diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell.tsx index 094050f2c3b4..ff1027a848cf 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell.tsx @@ -33,7 +33,7 @@ export const TableCell: React.FC = React.memo( const { action: { flattenedField }, - field: { field, fieldMapping, fieldType, scripted, pinned }, + field: { field, fieldMapping, fieldType, scripted }, value: { formattedValue, ignored }, } = row; @@ -49,7 +49,6 @@ export const TableCell: React.FC = React.memo( fieldMapping?.displayName ?? field, searchTerm )} - isPinned={pinned} /> {isDetails && !!fieldMapping ? ( diff --git a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx index 7814405e0920..b5e27837f44e 100644 --- a/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx +++ b/src/plugins/unified_doc_viewer/public/components/doc_viewer_table/table_cell_actions.tsx @@ -202,38 +202,6 @@ export const FilterExist: React.FC = ({ Component, row }) => ); }; -export const PinToggle: React.FC = ({ Component, row }) => { - if (!row) { - return null; - } - - const { - field: { field, pinned, onTogglePinned }, - } = row; - - // Pinned - const pinnedLabel = pinned - ? i18n.translate('unifiedDocViewer.docViews.table.unpinFieldLabel', { - defaultMessage: 'Unpin field', - }) - : i18n.translate('unifiedDocViewer.docViews.table.pinFieldLabel', { - defaultMessage: 'Pin field', - }); - const pinnedIconType = pinned ? 'pinFilled' : 'pin'; - - return ( - onTogglePinned(field)} - > - {pinnedLabel} - - ); -}; - export const ToggleColumn: React.FC = ({ Component, row }) => { if (!row) { return null; @@ -293,9 +261,6 @@ export function getFieldCellActions({ }, ] : []), - ({ Component, rowIndex }: EuiDataGridColumnCellActionProps) => { - return ; - }, ]; } diff --git a/test/functional/apps/discover/group3/_doc_viewer.ts b/test/functional/apps/discover/group3/_doc_viewer.ts index 66f1f74a4ddb..0464f2e4f32d 100644 --- a/test/functional/apps/discover/group3/_doc_viewer.ts +++ b/test/functional/apps/discover/group3/_doc_viewer.ts @@ -24,10 +24,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const dataGrid = getService('dataGrid'); const monacoEditor = getService('monacoEditor'); + const browser = getService('browser'); describe('discover doc viewer', function describeIndexTests() { before(async function () { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await browser.setWindowSize(1600, 1200); }); beforeEach(async () => { @@ -174,7 +176,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(initialFieldsCount).to.above(numberFieldsCount); const pinnedFieldsCount = 1; - await dataGrid.clickFieldActionInFlyout('agent', 'togglePinFilterButton'); + await dataGrid.togglePinActionInFlyout('agent'); await PageObjects.discover.openFilterByFieldTypeInDocViewer(); expect(await find.allByCssSelector('[data-test-subj*="typeFilter"]')).to.have.length(6); @@ -229,5 +231,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); }); + + describe('pinning fields', function () { + it('should be able to pin and unpin fields', async function () { + await dataGrid.clickRowToggle(); + await PageObjects.discover.isShowingDocViewer(); + await retry.waitFor('rendered items', async () => { + return (await find.allByCssSelector('.kbnDocViewer__fieldName')).length > 0; + }); + + let fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + let fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + + expect(fieldNames.join(',').startsWith('_id,_ignored,_index,_score,@message')).to.be(true); + expect(await dataGrid.isFieldPinnedInFlyout('agent')).to.be(false); + + await dataGrid.togglePinActionInFlyout('agent'); + + fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + expect(fieldNames.join(',').startsWith('agent,_id,_ignored')).to.be(true); + expect(await dataGrid.isFieldPinnedInFlyout('agent')).to.be(true); + + await dataGrid.togglePinActionInFlyout('@message'); + + fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + expect(fieldNames.join(',').startsWith('@message,agent,_id,_ignored')).to.be(true); + expect(await dataGrid.isFieldPinnedInFlyout('@message')).to.be(true); + + await dataGrid.togglePinActionInFlyout('@message'); + + fieldNameCells = await find.allByCssSelector('.kbnDocViewer__fieldName'); + fieldNames = await Promise.all(fieldNameCells.map((cell) => cell.getVisibleText())); + expect(fieldNames.join(',').startsWith('agent,_id,_ignored')).to.be(true); + expect(await dataGrid.isFieldPinnedInFlyout('agent')).to.be(true); + expect(await dataGrid.isFieldPinnedInFlyout('@message')).to.be(false); + }); + }); }); } diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index 70a67d33ffd0..fc6ed65631ea 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -567,6 +567,24 @@ export class DataGridService extends FtrService { await this.testSubjects.click(`${actionName}-${fieldName}`); } + public async isFieldPinnedInFlyout(fieldName: string): Promise { + return !( + await this.testSubjects.getAttribute(`unifiedDocViewer_pinControl_${fieldName}`, 'class') + )?.includes('kbnDocViewer__fieldsGrid__pinAction'); + } + + public async togglePinActionInFlyout(fieldName: string): Promise { + await this.testSubjects.moveMouseTo(`unifiedDocViewer_pinControl_${fieldName}`); + const isPinned = await this.isFieldPinnedInFlyout(fieldName); + await this.retry.waitFor('pin action to appear', async () => { + return this.testSubjects.exists(`unifiedDocViewer_pinControlButton_${fieldName}`); + }); + await this.testSubjects.click(`unifiedDocViewer_pinControlButton_${fieldName}`); + await this.retry.waitFor('pin action to toggle', async () => { + return (await this.isFieldPinnedInFlyout(fieldName)) !== isPinned; + }); + } + public async expandFieldNameCellInFlyout(fieldName: string): Promise { const buttonSelector = 'euiDataGridCellExpandButton'; await this.testSubjects.click(`tableDocViewRow-${fieldName}-name`); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index af2ab4bc10df..2fa4a66e8b37 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -7929,7 +7929,6 @@ "unifiedDocViewer.json.codeEditorAriaLabel": "Affichage JSON en lecture seule d’un document Elasticsearch", "unifiedDocViewer.json.copyToClipboardLabel": "Copier dans le presse-papiers", "unifiedDocViewer.loadingJSON": "Chargement de JSON", - "unifiedDocViewer.pinnedFieldTooltipContent": "Champ épinglé", "unifiedDocViewer.sourceViewer.errorMessage": "Impossible de récupérer les données pour le moment. Actualisez l'onglet et réessayez.", "unifiedDocViewer.sourceViewer.errorMessageTitle": "Une erreur s'est produite.", "unifiedDocViewer.sourceViewer.refresh": "Actualiser", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 602e8ea6b08a..c26fe66d0393 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7923,7 +7923,6 @@ "unifiedDocViewer.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", "unifiedDocViewer.json.copyToClipboardLabel": "クリップボードにコピー", "unifiedDocViewer.loadingJSON": "JSONを読み込んでいます", - "unifiedDocViewer.pinnedFieldTooltipContent": "固定されたフィールド", "unifiedDocViewer.sourceViewer.errorMessage": "現在データを取得できませんでした。タブを更新して、再試行してください。", "unifiedDocViewer.sourceViewer.errorMessageTitle": "エラーが発生しました", "unifiedDocViewer.sourceViewer.refresh": "更新", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 21e15ecd10f5..c4f2618a499d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7936,7 +7936,6 @@ "unifiedDocViewer.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", "unifiedDocViewer.json.copyToClipboardLabel": "复制到剪贴板", "unifiedDocViewer.loadingJSON": "正在加载 JSON", - "unifiedDocViewer.pinnedFieldTooltipContent": "已固定字段", "unifiedDocViewer.sourceViewer.errorMessage": "当前无法获取数据。请刷新选项卡以重试。", "unifiedDocViewer.sourceViewer.errorMessageTitle": "发生错误", "unifiedDocViewer.sourceViewer.refresh": "刷新", From 1e64b9e4b2903e797003766b604e0f8f35c51326 Mon Sep 17 00:00:00 2001 From: Jill Guyonnet Date: Mon, 19 Aug 2024 15:22:28 +0100 Subject: [PATCH 14/25] [Fleet] RBAC - Make upgrade agent APIs space aware (#190069) ## Summary Relates to https://github.com/elastic/kibana/issues/185040 This PR makes the following Fleet agents API space aware (behind `useSpaceAwareness` feature flag): * `POST /agents/{agentId}/reassign` * `POST /agents/{agentId}/upgrade` * `POST /agents/bulk_reassign` * `POST /agents/bulk_upgrade` * `POST /agents/{agentId}/actions/{actionId}/cancel` While working on that last endpoint, I noticed and fixed an error in the documentation. ### 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 - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --------- Co-authored-by: Nicolas Chaulet Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../plugins/fleet/common/openapi/bundled.json | 10 +- .../plugins/fleet/common/openapi/bundled.yaml | 7 +- .../fleet/common/openapi/entrypoint.yaml | 4 +- ...=> agents@actions@{action_id}@cancel.yaml} | 5 - .../server/routes/agent/actions_handlers.ts | 10 +- .../fleet/server/routes/agent/handlers.ts | 2 +- .../fleet/server/routes/agent/index.ts | 4 +- .../server/services/agents/action_runner.ts | 14 +- .../server/services/agents/actions.test.ts | 17 +- .../fleet/server/services/agents/actions.ts | 43 +- .../fleet/server/services/agents/crud.ts | 6 +- .../server/services/agents/reassign.test.ts | 19 +- .../fleet/server/services/agents/reassign.ts | 92 ++-- .../services/agents/reassign_action_runner.ts | 12 +- .../services/agents/update_agent_tags.test.ts | 24 -- .../services/agents/update_agent_tags.ts | 19 +- .../agents/update_agent_tags_action_runner.ts | 12 +- .../server/services/agents/upgrade.test.ts | 21 +- .../fleet/server/services/agents/upgrade.ts | 18 +- .../services/agents/upgrade_action_runner.ts | 17 +- .../apis/space_awareness/actions.ts | 55 ++- .../apis/space_awareness/agents.ts | 402 ++++++++++++++++-- .../apis/space_awareness/api_helper.ts | 41 +- .../apis/space_awareness/helpers.ts | 33 +- 24 files changed, 693 insertions(+), 194 deletions(-) rename x-pack/plugins/fleet/common/openapi/paths/{agents@{agent_id}@actions@{action_id}@cancel.yaml => agents@actions@{action_id}@cancel.yaml} (87%) diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index af5420aaad64..989955b748ed 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -2520,16 +2520,8 @@ } } }, - "/agents/{agentId}/actions/{actionId}/cancel": { + "/agents/actions/{actionId}/cancel": { "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentId", - "in": "path", - "required": true - }, { "schema": { "type": "string" diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 9bb1027ef35c..fe1c2fc68c58 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -1580,13 +1580,8 @@ paths: properties: action: $ref: '#/components/schemas/agent_action' - /agents/{agentId}/actions/{actionId}/cancel: + /agents/actions/{actionId}/cancel: parameters: - - schema: - type: string - name: agentId - in: path - required: true - schema: type: string name: actionId diff --git a/x-pack/plugins/fleet/common/openapi/entrypoint.yaml b/x-pack/plugins/fleet/common/openapi/entrypoint.yaml index 2de74e31a9a3..1ba15cb190f1 100644 --- a/x-pack/plugins/fleet/common/openapi/entrypoint.yaml +++ b/x-pack/plugins/fleet/common/openapi/entrypoint.yaml @@ -91,8 +91,8 @@ paths: $ref: 'paths/agents@{agent_id}.yaml' '/agents/{agentId}/actions': $ref: 'paths/agents@{agent_id}@actions.yaml' - '/agents/{agentId}/actions/{actionId}/cancel': - $ref: 'paths/agents@{agent_id}@actions@{action_id}@cancel.yaml' + '/agents/actions/{actionId}/cancel': + $ref: 'paths/agents@actions@{action_id}@cancel.yaml' '/agents/files/{fileId}/{fileName}': $ref: 'paths/agents@files@{file_id}@{file_name}.yaml' '/agents/files/{fileId}': diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@actions@{action_id}@cancel.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents@actions@{action_id}@cancel.yaml similarity index 87% rename from x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@actions@{action_id}@cancel.yaml rename to x-pack/plugins/fleet/common/openapi/paths/agents@actions@{action_id}@cancel.yaml index 5b939e8c5fdf..d9ee5127e4b0 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agents@{agent_id}@actions@{action_id}@cancel.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agents@actions@{action_id}@cancel.yaml @@ -1,9 +1,4 @@ parameters: - - schema: - type: string - name: agentId - in: path - required: true - schema: type: string name: actionId diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts index 9b5bbdccb354..b11dcb719e2d 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts @@ -59,9 +59,15 @@ export const postCancelActionHandlerBuilder = function ( ): RequestHandler, undefined, undefined> { return async (context, request, response) => { try { - const esClient = (await context.core).elasticsearch.client.asInternalUser; + const core = await context.core; + const esClient = core.elasticsearch.client.asInternalUser; + const soClient = core.savedObjects.client; - const action = await actionsService.cancelAgentAction(esClient, request.params.actionId); + const action = await actionsService.cancelAgentAction( + esClient, + soClient, + request.params.actionId + ); const body: PostNewAgentActionResponse = { item: action, diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 350eb24847d8..e328c7387898 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -266,7 +266,7 @@ export const putAgentsReassignHandlerDeprecated: RequestHandler< } }; -export const postAgentsReassignHandler: RequestHandler< +export const postAgentReassignHandler: RequestHandler< TypeOf, undefined, TypeOf diff --git a/x-pack/plugins/fleet/server/routes/agent/index.ts b/x-pack/plugins/fleet/server/routes/agent/index.ts index 6c55835a1ed9..7d64bf365f74 100644 --- a/x-pack/plugins/fleet/server/routes/agent/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent/index.ts @@ -59,7 +59,7 @@ import { getAgentUploadsHandler, getAgentUploadFileHandler, deleteAgentUploadFileHandler, - postAgentsReassignHandler, + postAgentReassignHandler, postRetrieveAgentsByActionsHandler, } from './handlers'; import { @@ -271,7 +271,7 @@ export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigT version: API_VERSIONS.public.v1, validate: { request: PostAgentReassignRequestSchema }, }, - postAgentsReassignHandler + postAgentReassignHandler ); router.versioned diff --git a/x-pack/plugins/fleet/server/services/agents/action_runner.ts b/x-pack/plugins/fleet/server/services/agents/action_runner.ts index 3abe4787e613..f7eea6f3ac56 100644 --- a/x-pack/plugins/fleet/server/services/agents/action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/action_runner.ts @@ -16,6 +16,7 @@ import moment from 'moment'; import type { Agent } from '../../types'; import { appContextService } from '..'; import { SO_SEARCH_LIMIT } from '../../../common/constants'; +import { agentsKueryNamespaceFilter } from '../spaces/agent_namespaces'; import { getAgentActions } from './actions'; import { closePointInTime, getAgentsByKuery } from './crud'; @@ -29,6 +30,7 @@ export interface ActionParams { batchSize?: number; total?: number; actionId?: string; + spaceId?: string; // additional parameters specific to an action e.g. reassign to new policy id [key: string]: any; } @@ -195,15 +197,21 @@ export abstract class ActionRunner { appContextService.getLogger().debug('kuery: ' + this.actionParams.kuery); - const getAgents = () => - getAgentsByKuery(this.esClient, this.soClient, { - kuery: this.actionParams.kuery, + const getAgents = async () => { + const namespaceFilter = await agentsKueryNamespaceFilter(this.actionParams.spaceId); + const kuery = namespaceFilter + ? `${namespaceFilter} AND ${this.actionParams.kuery}` + : this.actionParams.kuery; + + return getAgentsByKuery(this.esClient, this.soClient, { + kuery, showInactive: this.actionParams.showInactive ?? false, page: 1, perPage, pitId, searchAfter: this.retryParams.searchAfter, }); + }; const res = await getAgents(); diff --git a/x-pack/plugins/fleet/server/services/agents/actions.test.ts b/x-pack/plugins/fleet/server/services/agents/actions.test.ts index 4a2fc9e743b2..b8cb2ce8c8d6 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; import type { NewAgentAction, AgentActionType } from '../../../common/types'; @@ -307,16 +307,17 @@ describe('Agent actions', () => { }); describe('cancelAgentAction', () => { - it('throw if the target action is not found', async () => { + it('should throw if the target action is not found', async () => { const esClient = elasticsearchServiceMock.createInternalClient(); esClient.search.mockResolvedValue({ hits: { hits: [], }, } as any); - await expect(() => cancelAgentAction(esClient, 'i-do-not-exists')).rejects.toThrowError( - /Action not found/ - ); + const soClient = savedObjectsClientMock.create(); + await expect(() => + cancelAgentAction(esClient, soClient, 'i-do-not-exists') + ).rejects.toThrowError(/Action not found/); }); it('should create one CANCEL action for each UPGRADE action found', async () => { @@ -343,7 +344,8 @@ describe('Agent actions', () => { ], }, } as any); - await cancelAgentAction(esClient, 'action1'); + const soClient = savedObjectsClientMock.create(); + await cancelAgentAction(esClient, soClient, 'action1'); expect(esClient.create).toBeCalledTimes(2); expect(esClient.create).toBeCalledWith( @@ -382,7 +384,8 @@ describe('Agent actions', () => { ], }, } as any); - await cancelAgentAction(esClient, 'action1'); + const soClient = savedObjectsClientMock.create(); + await cancelAgentAction(esClient, soClient, 'action1'); expect(mockedBulkUpdateAgents).toBeCalled(); expect(mockedBulkUpdateAgents).toBeCalledWith( diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index f344aa24e59d..11c7174e0931 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -30,6 +30,9 @@ import { auditLoggingService } from '../audit_logging'; import { getAgentIdsForAgentPolicies } from '../agent_policies/agent_policies_to_agent_ids'; +import { getCurrentNamespace } from '../spaces/get_current_namespace'; +import { addNamespaceFilteringToQuery } from '../spaces/query_namespaces_filtering'; + import { bulkUpdateAgents } from './crud'; const ONE_MONTH_IN_MS = 2592000000; @@ -305,21 +308,28 @@ export async function getUnenrollAgentActions( return result; } -export async function cancelAgentAction(esClient: ElasticsearchClient, actionId: string) { +export async function cancelAgentAction( + esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract, + actionId: string +) { + const currentNameSpace = getCurrentNamespace(soClient); + const getUpgradeActions = async () => { - const res = await esClient.search({ - index: AGENT_ACTIONS_INDEX, - query: { - bool: { - filter: [ - { - term: { - action_id: actionId, - }, + const query = { + bool: { + filter: [ + { + term: { + action_id: actionId, }, - ], - }, + }, + ], }, + }; + const res = await esClient.search({ + index: AGENT_ACTIONS_INDEX, + query: await addNamespaceFilteringToQuery(query, currentNameSpace), size: SO_SEARCH_LIMIT, }); @@ -348,9 +358,12 @@ export async function cancelAgentAction(esClient: ElasticsearchClient, actionId: const cancelledActions: Array<{ agents: string[] }> = []; const createAction = async (action: FleetServerAgentAction) => { + const namespaces = currentNameSpace ? { namespaces: [currentNameSpace] } : {}; + await createAgentAction(esClient, { id: cancelActionId, type: 'CANCEL', + ...namespaces, agents: action.agents!, data: { target_id: action.action_id, @@ -505,7 +518,11 @@ export interface ActionsService { agentId: string ) => Promise; - cancelAgentAction: (esClient: ElasticsearchClient, actionId: string) => Promise; + cancelAgentAction: ( + esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract, + actionId: string + ) => Promise; createAgentAction: ( esClient: ElasticsearchClient, diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 7fdf76c76992..847d0dd8335c 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -27,9 +27,10 @@ import { FleetUnauthorizedError, } from '../../errors'; import { auditLoggingService } from '../audit_logging'; -import { isAgentInNamespace } from '../spaces/agent_namespaces'; import { getCurrentNamespace } from '../spaces/get_current_namespace'; import { isSpaceAwarenessEnabled } from '../spaces/helpers'; +import { isAgentInNamespace } from '../spaces/agent_namespaces'; +import { addNamespaceFilteringToQuery } from '../spaces/query_namespaces_filtering'; import { searchHitToAgent, agentSOAttributesToFleetServerAgentDoc } from './helpers'; import { buildAgentStatusRuntimeField } from './build_status_runtime_field'; @@ -432,6 +433,7 @@ async function _filterAgents( }> { const { page = 1, perPage = 20, sortField = 'enrolled_at', sortOrder = 'desc' } = options; const runtimeFields = await buildAgentStatusRuntimeField(soClient); + const currentNameSpace = getCurrentNamespace(soClient); let res; try { @@ -443,7 +445,7 @@ async function _filterAgents( runtime_mappings: runtimeFields, fields: Object.keys(runtimeFields), sort: [{ [sortField]: { order: sortOrder } }], - query: { bool: { filter: query } }, + query: await addNamespaceFilteringToQuery({ bool: { filter: [query] } }, currentNameSpace), index: AGENTS_INDEX, ignore_unavailable: true, }); diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts index 4ae78e07d3a8..5d55fdd5da31 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts @@ -15,11 +15,15 @@ import { reassignAgent, reassignAgents } from './reassign'; import { createClientMock } from './action.mock'; describe('reassignAgent', () => { + let mocks: ReturnType; + beforeEach(async () => { - const { soClient } = createClientMock(); + mocks = createClientMock(); + appContextService.start( createAppContextStartContractMock({}, false, { - withoutSpaceExtensions: soClient, + internal: mocks.soClient, + withoutSpaceExtensions: mocks.soClient, }) ); }); @@ -29,7 +33,7 @@ describe('reassignAgent', () => { }); describe('reassignAgent (singular)', () => { it('can reassign from regular agent policy to regular', async () => { - const { soClient, esClient, agentInRegularDoc, regularAgentPolicySO } = createClientMock(); + const { soClient, esClient, agentInRegularDoc, regularAgentPolicySO } = mocks; await reassignAgent(soClient, esClient, agentInRegularDoc._id, regularAgentPolicySO.id); // calls ES update with correct values @@ -43,7 +47,7 @@ describe('reassignAgent', () => { }); it('cannot reassign from regular agent policy to hosted', async () => { - const { soClient, esClient, agentInRegularDoc, hostedAgentPolicySO } = createClientMock(); + const { soClient, esClient, agentInRegularDoc, hostedAgentPolicySO } = mocks; await expect( reassignAgent(soClient, esClient, agentInRegularDoc._id, hostedAgentPolicySO.id) ).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError); @@ -54,7 +58,7 @@ describe('reassignAgent', () => { it('cannot reassign from hosted agent policy', async () => { const { soClient, esClient, agentInHostedDoc, hostedAgentPolicySO, regularAgentPolicySO } = - createClientMock(); + mocks; await expect( reassignAgent(soClient, esClient, agentInHostedDoc._id, regularAgentPolicySO.id) ).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError); @@ -78,7 +82,7 @@ describe('reassignAgent', () => { agentInHostedDoc, agentInHostedDoc2, regularAgentPolicySO2, - } = createClientMock(); + } = mocks; esClient.search.mockResponse({ hits: { @@ -116,7 +120,8 @@ describe('reassignAgent', () => { }); it('should report errors from ES agent update call', async () => { - const { soClient, esClient, agentInRegularDoc, regularAgentPolicySO2 } = createClientMock(); + const { soClient, esClient, agentInRegularDoc, regularAgentPolicySO2 } = mocks; + esClient.bulk.mockResponse({ items: [ { diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index 0a5c6f9b51ee..d5a4d2ab6752 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -6,6 +6,8 @@ */ import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; +import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common'; + import type { Agent } from '../../types'; import { agentPolicyService } from '../agent_policy'; import { @@ -16,52 +18,54 @@ import { import { SO_SEARCH_LIMIT } from '../../constants'; +import { agentsKueryNamespaceFilter } from '../spaces/agent_namespaces'; +import { getCurrentNamespace } from '../spaces/get_current_namespace'; + import { getAgentsById, getAgentPolicyForAgent, updateAgent, getAgentsByKuery, openPointInTime, + getAgentById, } from './crud'; import type { GetAgentsOptions } from '.'; import { createAgentAction } from './actions'; import { ReassignActionRunner, reassignBatch } from './reassign_action_runner'; -export async function reassignAgent( +async function verifyNewAgentPolicy( soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - agentId: string, newAgentPolicyId: string ) { - const newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); + let newAgentPolicy; + try { + newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); + } catch (err) { + if (err instanceof SavedObjectNotFound) { + throw new AgentPolicyNotFoundError(`Agent policy not found: ${newAgentPolicyId}`); + } + } if (!newAgentPolicy) { throw new AgentPolicyNotFoundError(`Agent policy not found: ${newAgentPolicyId}`); } - - await reassignAgentIsAllowed(soClient, esClient, agentId, newAgentPolicyId); - - await updateAgent(esClient, agentId, { - policy_id: newAgentPolicyId, - policy_revision: null, - }); - - await createAgentAction(esClient, { - agents: [agentId], - created_at: new Date().toISOString(), - type: 'POLICY_REASSIGN', - data: { - policy_id: newAgentPolicyId, - }, - }); + if (newAgentPolicy?.is_managed) { + throw new HostedAgentPolicyRestrictionRelatedError( + `Cannot reassign agents to hosted agent policy ${newAgentPolicy.id}` + ); + } } -export async function reassignAgentIsAllowed( +export async function reassignAgent( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, agentId: string, newAgentPolicyId: string ) { + await verifyNewAgentPolicy(soClient, newAgentPolicyId); + + await getAgentById(esClient, soClient, agentId); // throw 404 if agent not in namespace + const agentPolicy = await getAgentPolicyForAgent(soClient, esClient, agentId); if (agentPolicy?.is_managed) { throw new HostedAgentPolicyRestrictionRelatedError( @@ -69,14 +73,23 @@ export async function reassignAgentIsAllowed( ); } - const newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); - if (newAgentPolicy?.is_managed) { - throw new HostedAgentPolicyRestrictionRelatedError( - `Cannot reassign an agent to hosted agent policy ${newAgentPolicy.id}` - ); - } + await updateAgent(esClient, agentId, { + policy_id: newAgentPolicyId, + policy_revision: null, + }); + + const currentNameSpace = getCurrentNamespace(soClient); + const namespaces = currentNameSpace ? { namespaces: [currentNameSpace] } : {}; - return true; + await createAgentAction(esClient, { + agents: [agentId], + created_at: new Date().toISOString(), + type: 'POLICY_REASSIGN', + data: { + policy_id: newAgentPolicyId, + }, + ...namespaces, + }); } export async function reassignAgents( @@ -88,16 +101,9 @@ export async function reassignAgents( }, newAgentPolicyId: string ): Promise<{ actionId: string }> { - const newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); - if (!newAgentPolicy) { - throw new AgentPolicyNotFoundError(`Agent policy not found: ${newAgentPolicyId}`); - } - if (newAgentPolicy.is_managed) { - throw new HostedAgentPolicyRestrictionRelatedError( - `Cannot reassign an agent to hosted agent policy ${newAgentPolicy.id}` - ); - } + await verifyNewAgentPolicy(soClient, newAgentPolicyId); + const currentNameSpace = getCurrentNamespace(soClient); const outgoingErrors: Record = {}; let givenAgents: Agent[] = []; if ('agents' in options) { @@ -115,8 +121,10 @@ export async function reassignAgents( } } else if ('kuery' in options) { const batchSize = options.batchSize ?? SO_SEARCH_LIMIT; + const namespaceFilter = await agentsKueryNamespaceFilter(currentNameSpace); + const kuery = namespaceFilter ? `${namespaceFilter} AND ${options.kuery}` : options.kuery; const res = await getAgentsByKuery(esClient, soClient, { - kuery: options.kuery, + kuery, showInactive: options.showInactive ?? false, page: 1, perPage: batchSize, @@ -130,6 +138,7 @@ export async function reassignAgents( soClient, { ...options, + spaceId: currentNameSpace, batchSize, total: res.total, newAgentPolicyId, @@ -139,5 +148,10 @@ export async function reassignAgents( } } - return await reassignBatch(soClient, esClient, { newAgentPolicyId }, givenAgents, outgoingErrors); + return await reassignBatch( + esClient, + { newAgentPolicyId, spaceId: currentNameSpace }, + givenAgents, + outgoingErrors + ); } diff --git a/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts index b03146ab6b38..cd9183b0771b 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign_action_runner.ts @@ -5,7 +5,7 @@ * 2.0. */ import { v4 as uuidv4 } from 'uuid'; -import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; +import type { ElasticsearchClient } from '@kbn/core/server'; import type { Agent } from '../../types'; @@ -22,7 +22,7 @@ import { BulkActionTaskType } from './bulk_action_types'; export class ReassignActionRunner extends ActionRunner { protected async processAgents(agents: Agent[]): Promise<{ actionId: string }> { - return await reassignBatch(this.soClient, this.esClient, this.actionParams! as any, agents, {}); + return await reassignBatch(this.esClient, this.actionParams! as any, agents, {}); } protected getTaskType() { @@ -35,16 +35,18 @@ export class ReassignActionRunner extends ActionRunner { } export async function reassignBatch( - soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, options: { newAgentPolicyId: string; actionId?: string; total?: number; + spaceId?: string; }, givenAgents: Agent[], outgoingErrors: Record ): Promise<{ actionId: string }> { + const spaceId = options.spaceId; + const soClient = appContextService.getInternalUserSOClientForSpaceId(spaceId); const errors: Record = { ...outgoingErrors }; const hostedPolicies = await getHostedPolicies(soClient, givenAgents); @@ -86,8 +88,9 @@ export async function reassignBatch( const actionId = options.actionId ?? uuidv4(); const total = options.total ?? givenAgents.length; - const now = new Date().toISOString(); + const namespaces = spaceId ? { namespaces: [spaceId] } : {}; + await createAgentAction(esClient, { id: actionId, agents: agentsToUpdate.map((agent) => agent.id), @@ -97,6 +100,7 @@ export async function reassignBatch( data: { policy_id: options.newAgentPolicyId, }, + ...namespaces, }); await createErrorActionResults( diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts index efeb5649cd57..cd408d953e31 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts @@ -423,30 +423,6 @@ describe('update_agent_tags', () => { jest.mocked(isSpaceAwarenessEnabled).mockResolvedValue(true); }); - it('should not update tags for agents in another space', async () => { - soClient.getCurrentNamespace.mockReturnValue('default'); - esClient.search.mockResolvedValue({ - hits: { - hits: [ - { - _id: 'agent1', - _source: { - tags: ['one', 'two', 'three'], - namespaces: ['myspace'], - }, - fields: { - status: 'online', - }, - }, - ], - }, - } as any); - - await updateAgentTags(soClient, esClient, { agentIds: ['agent1'] }, ['one'], ['two']); - - expect(esClient.updateByQuery).not.toHaveBeenCalled(); - }); - it('should add namespace filter to kuery in the default space', async () => { soClient.getCurrentNamespace.mockReturnValue('default'); diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts index 7d37581cef99..4e42ac121ccc 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts @@ -27,9 +27,9 @@ export async function updateAgentTags( tagsToAdd: string[], tagsToRemove: string[] ): Promise<{ actionId: string }> { + const currentNameSpace = getCurrentNamespace(soClient); const outgoingErrors: Record = {}; const givenAgents: Agent[] = []; - const currentNameSpace = getCurrentNamespace(soClient); if ('agentIds' in options) { const maybeAgents = await getAgentsById(esClient, soClient, options.agentIds); @@ -48,8 +48,8 @@ export async function updateAgentTags( } } else if ('kuery' in options) { const batchSize = options.batchSize ?? SO_SEARCH_LIMIT; - const namespaceFilter = await agentsKueryNamespaceFilter(currentNameSpace); + const filters = namespaceFilter ? [namespaceFilter] : []; if (options.kuery !== '') { filters.push(options.kuery); @@ -86,8 +86,15 @@ export async function updateAgentTags( ).runActionAsyncWithRetry(); } - return await updateTagsBatch(soClient, esClient, givenAgents, outgoingErrors, { - tagsToAdd, - tagsToRemove, - }); + return await updateTagsBatch( + soClient, + esClient, + givenAgents, + outgoingErrors, + { + tagsToAdd, + tagsToRemove, + }, + currentNameSpace + ); } diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts index 8b68e1b6e9fd..bb3b5f71cb22 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts @@ -17,8 +17,6 @@ import { appContextService } from '../app_context'; import { FleetError } from '../../errors'; -import { getCurrentNamespace } from '../spaces/get_current_namespace'; - import { ActionRunner } from './action_runner'; import { BulkActionTaskType } from './bulk_action_types'; @@ -63,7 +61,8 @@ export async function updateTagsBatch( total?: number; kuery?: string; retryCount?: number; - } + }, + spaceId?: string ): Promise<{ actionId: string; updated?: number; took?: number }> { const errors: Record = { ...outgoingErrors }; const hostedAgentError = `Cannot modify tags on a hosted agent`; @@ -151,8 +150,7 @@ export async function updateTagsBatch( const versionConflictCount = res.version_conflicts ?? 0; const versionConflictIds = isLastRetry ? getUuidArray(versionConflictCount) : []; - const currentNameSpace = getCurrentNamespace(soClient); - const namespaces = currentNameSpace ? { namespaces: [currentNameSpace] } : {}; + const namespaces = spaceId ? { namespaces: [spaceId] } : {}; // creating an action doc so that update tags shows up in activity // the logic only saves agent count in the action that updated, failed or in case of last retry, conflicted @@ -195,7 +193,7 @@ export async function updateTagsBatch( failures.map((failure) => ({ agentId: failure.id, actionId, - namespace: currentNameSpace, + namespace: spaceId, error: failure.cause.reason, })) ); @@ -210,7 +208,7 @@ export async function updateTagsBatch( versionConflictIds.map((id) => ({ agentId: id, actionId, - namespace: currentNameSpace, + namespace: spaceId, error: 'version conflict on last retry', })) ); diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.test.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.test.ts index b1e78862fde0..7dbfaf86bd27 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.test.ts @@ -35,11 +35,15 @@ jest.mock('./action_status', () => { }); describe('sendUpgradeAgentsActions (plural)', () => { + let mocks: ReturnType; + beforeEach(async () => { - const { soClient } = createClientMock(); + mocks = createClientMock(); + appContextService.start( createAppContextStartContractMock({}, false, { - withoutSpaceExtensions: soClient, + internal: mocks.soClient, + withoutSpaceExtensions: mocks.soClient, }) ); }); @@ -48,7 +52,7 @@ describe('sendUpgradeAgentsActions (plural)', () => { appContextService.stop(); }); it('can upgrade from an regular agent policy', async () => { - const { soClient, esClient, agentInRegularDoc, agentInRegularDoc2 } = createClientMock(); + const { soClient, esClient, agentInRegularDoc, agentInRegularDoc2 } = mocks; const idsToAction = [agentInRegularDoc._id, agentInRegularDoc2._id]; await sendUpgradeAgentsActions(soClient, esClient, { agentIds: idsToAction, version: '8.5.0' }); @@ -68,8 +72,7 @@ describe('sendUpgradeAgentsActions (plural)', () => { } }); it('cannot upgrade from a hosted agent policy by default', async () => { - const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } = - createClientMock(); + const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } = mocks; const idsToAction = [agentInRegularDoc._id, agentInHostedDoc._id, agentInRegularDoc2._id]; await sendUpgradeAgentsActions(soClient, esClient, { agentIds: idsToAction, version: '8.5.0' }); @@ -104,8 +107,8 @@ describe('sendUpgradeAgentsActions (plural)', () => { }); it('can upgrade from hosted agent policy with force=true', async () => { - const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } = - createClientMock(); + const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } = mocks; + const idsToAction = [agentInRegularDoc._id, agentInHostedDoc._id, agentInRegularDoc2._id]; await sendUpgradeAgentsActions(soClient, esClient, { agentIds: idsToAction, @@ -129,9 +132,9 @@ describe('sendUpgradeAgentsActions (plural)', () => { }); it('skip upgrade if action id is cancelled', async () => { - const { soClient, esClient, agentInRegularDoc } = createClientMock(); + const { esClient, agentInRegularDoc } = mocks; const agents = [{ id: agentInRegularDoc._id } as Agent]; - await upgradeBatch(soClient, esClient, agents, {}, { + await upgradeBatch(esClient, agents, {}, { actionId: 'cancelled-action', } as any); }); diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index a164b9ff8639..40d676a68e24 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -10,6 +10,9 @@ import type { Agent } from '../../types'; import { AgentReassignmentError, HostedAgentPolicyRestrictionRelatedError } from '../../errors'; import { SO_SEARCH_LIMIT } from '../../constants'; +import { agentsKueryNamespaceFilter } from '../spaces/agent_namespaces'; +import { getCurrentNamespace } from '../spaces/get_current_namespace'; + import { createAgentAction } from './actions'; import type { GetAgentsOptions } from './crud'; import { openPointInTime } from './crud'; @@ -43,12 +46,16 @@ export async function sendUpgradeAgentAction({ ); } + const currentNameSpace = getCurrentNamespace(soClient); + const namespaces = currentNameSpace ? { namespaces: [currentNameSpace] } : {}; + await createAgentAction(esClient, { agents: [agentId], created_at: now, data, ack_data: data, type: 'UPGRADE', + ...namespaces, }); await updateAgent(esClient, agentId, { upgraded_at: null, @@ -69,9 +76,11 @@ export async function sendUpgradeAgentsActions( batchSize?: number; } ): Promise<{ actionId: string }> { + const currentNameSpace = getCurrentNamespace(soClient); // Full set of agents const outgoingErrors: Record = {}; let givenAgents: Agent[] = []; + if ('agents' in options) { givenAgents = options.agents; } else if ('agentIds' in options) { @@ -87,12 +96,16 @@ export async function sendUpgradeAgentsActions( } } else if ('kuery' in options) { const batchSize = options.batchSize ?? SO_SEARCH_LIMIT; + const namespaceFilter = await agentsKueryNamespaceFilter(currentNameSpace); + const kuery = namespaceFilter ? `${namespaceFilter} AND ${options.kuery}` : options.kuery; + const res = await getAgentsByKuery(esClient, soClient, { - kuery: options.kuery, + kuery, showInactive: options.showInactive ?? false, page: 1, perPage: batchSize, }); + if (res.total <= batchSize) { givenAgents = res.agents; } else { @@ -103,11 +116,12 @@ export async function sendUpgradeAgentsActions( ...options, batchSize, total: res.total, + spaceId: currentNameSpace, }, { pitId: await openPointInTime(esClient) } ).runActionAsyncWithRetry(); } } - return await upgradeBatch(soClient, esClient, givenAgents, outgoingErrors, options); + return await upgradeBatch(esClient, givenAgents, outgoingErrors, options, currentNameSpace); } diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts index d123489fe9ea..a11b43a5b3ee 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade_action_runner.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; +import type { ElasticsearchClient } from '@kbn/core/server'; import { v4 as uuidv4 } from 'uuid'; import moment from 'moment'; @@ -34,7 +34,13 @@ import { getLatestAvailableAgentVersion } from './versions'; export class UpgradeActionRunner extends ActionRunner { protected async processAgents(agents: Agent[]): Promise<{ actionId: string }> { - return await upgradeBatch(this.soClient, this.esClient, agents, {}, this.actionParams! as any); + return await upgradeBatch( + this.esClient, + agents, + {}, + this.actionParams! as any, + this.actionParams?.spaceId + ); } protected getTaskType() { @@ -52,7 +58,6 @@ const isActionIdCancelled = async (esClient: ElasticsearchClient, actionId: stri }; export async function upgradeBatch( - soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, givenAgents: Agent[], outgoingErrors: Record, @@ -65,8 +70,10 @@ export async function upgradeBatch( upgradeDurationSeconds?: number; startTime?: string; total?: number; - } + }, + spaceId?: string ): Promise<{ actionId: string }> { + const soClient = appContextService.getInternalUserSOClientForSpaceId(spaceId); const errors: Record = { ...outgoingErrors }; const hostedPolicies = await getHostedPolicies(soClient, givenAgents); @@ -167,6 +174,7 @@ export async function upgradeBatch( const actionId = options.actionId ?? uuidv4(); const total = options.total ?? givenAgents.length; + const namespaces = spaceId ? { namespaces: [spaceId] } : {}; await createAgentAction(esClient, { id: actionId, @@ -177,6 +185,7 @@ export async function upgradeBatch( total, agents: agentsToUpdate.map((agent) => agent.id), ...rollingUpgradeOptions, + ...namespaces, }); await createErrorActionResults( diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts index 4f458cd7190c..14c3dff33895 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts @@ -19,8 +19,7 @@ export default function (providerContext: FtrProviderContext) { const esClient = getService('es'); const kibanaServer = getService('kibanaServer'); - // Failing: See https://github.com/elastic/kibana/issues/189805 - describe.skip('actions', async function () { + describe('actions', async function () { skipIfNoDockerRegistry(providerContext); const apiClient = new SpaceTestApiClient(supertest); @@ -221,16 +220,14 @@ export default function (providerContext: FtrProviderContext) { .set('kbn-xsrf', 'xxxx') .send({ action: { type: 'UNENROLL' } }) .expect(404); - expect(resInDefaultSpace.body.message).to.eql(`${testSpaceAgent1} not found in namespace`); + expect(resInDefaultSpace.body.message).to.eql(`Agent ${testSpaceAgent1} not found`); const resInCustomSpace = await supertest .post(`/s/${TEST_SPACE_1}/api/fleet/agents/${defaultSpaceAgent1}/actions`) .set('kbn-xsrf', 'xxxx') .send({ action: { type: 'UNENROLL' } }) .expect(404); - expect(resInCustomSpace.body.message).to.eql( - `${defaultSpaceAgent1} not found in namespace` - ); + expect(resInCustomSpace.body.message).to.eql(`Agent ${defaultSpaceAgent1} not found`); }); it('should create an action with set namespace in the default space', async () => { @@ -253,5 +250,51 @@ export default function (providerContext: FtrProviderContext) { expect(actionStatusInCustomSpace.items.length).to.eql(1); }); }); + + describe('post /agents/actions/{actionId}/cancel', () => { + it('should return 200 and a CANCEL action if the action is in the same space', async () => { + // Create UPDATE_TAGS action for agents in custom space + await apiClient.bulkUpdateAgentTags( + { + agents: [testSpaceAgent1, testSpaceAgent2], + tagsToAdd: ['tag1'], + }, + TEST_SPACE_1 + ); + + const actionStatusInCustomSpace = await apiClient.getActionStatus(TEST_SPACE_1); + expect(actionStatusInCustomSpace.items.length).to.eql(1); + + const res = await apiClient.cancelAction( + actionStatusInCustomSpace.items[0].actionId, + TEST_SPACE_1 + ); + expect(res.item.type).to.eql('CANCEL'); + }); + + it('should return 404 if the action is in a different space', async () => { + // Create UPDATE_TAGS action for agents in custom space + await apiClient.bulkUpdateAgentTags( + { + agents: [testSpaceAgent1, testSpaceAgent2], + tagsToAdd: ['tag1'], + }, + TEST_SPACE_1 + ); + + const actionStatusInCustomSpace = await apiClient.getActionStatus(TEST_SPACE_1); + expect(actionStatusInCustomSpace.items.length).to.eql(1); + + let err: Error | undefined; + try { + await apiClient.cancelAction(actionStatusInCustomSpace.items[0].actionId); + } catch (_err) { + err = _err; + } + + expect(err).to.be.an(Error); + expect(err?.message).to.match(/404 "Not Found"/); + }); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts index b4f7241dec0f..f41f83f71ccb 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts @@ -10,7 +10,12 @@ import { CreateAgentPolicyResponse, GetAgentsResponse } from '@kbn/fleet-plugin/ import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; import { SpaceTestApiClient } from './api_helper'; -import { cleanFleetIndices, createFleetAgent } from './helpers'; +import { + cleanFleetAgents, + cleanFleetIndices, + createFleetAgent, + makeAgentsUpgradeable, +} from './helpers'; import { setupTestSpaces, TEST_SPACE_1 } from './space_helpers'; export default function (providerContext: FtrProviderContext) { @@ -41,6 +46,7 @@ export default function (providerContext: FtrProviderContext) { setupTestSpaces(providerContext); let defaultSpacePolicy1: CreateAgentPolicyResponse; + let defaultSpacePolicy2: CreateAgentPolicyResponse; let spaceTest1Policy1: CreateAgentPolicyResponse; let spaceTest1Policy2: CreateAgentPolicyResponse; @@ -48,36 +54,50 @@ export default function (providerContext: FtrProviderContext) { let defaultSpaceAgent2: string; let testSpaceAgent1: string; let testSpaceAgent2: string; + let testSpaceAgent3: string; + + async function createAgents() { + const [ + _defaultSpaceAgent1, + _defaultSpaceAgent2, + _testSpaceAgent1, + _testSpaceAgent2, + _testSpaceAgent3, + ] = await Promise.all([ + createFleetAgent(esClient, defaultSpacePolicy1.item.id, 'default'), + createFleetAgent(esClient, defaultSpacePolicy2.item.id), + createFleetAgent(esClient, spaceTest1Policy1.item.id, TEST_SPACE_1), + createFleetAgent(esClient, spaceTest1Policy2.item.id, TEST_SPACE_1), + createFleetAgent(esClient, spaceTest1Policy1.item.id, TEST_SPACE_1), + ]); + defaultSpaceAgent1 = _defaultSpaceAgent1; + defaultSpaceAgent2 = _defaultSpaceAgent2; + testSpaceAgent1 = _testSpaceAgent1; + testSpaceAgent2 = _testSpaceAgent2; + testSpaceAgent3 = _testSpaceAgent3; + } before(async () => { await apiClient.postEnableSpaceAwareness(); - - const [_defaultSpacePolicy1, _spaceTest1Policy1, _spaceTest1Policy2] = await Promise.all([ - apiClient.createAgentPolicy(), - apiClient.createAgentPolicy(TEST_SPACE_1), - apiClient.createAgentPolicy(TEST_SPACE_1), - ]); + const [_defaultSpacePolicy1, _defaultSpacePolicy2, _spaceTest1Policy1, _spaceTest1Policy2] = + await Promise.all([ + apiClient.createAgentPolicy(), + apiClient.createAgentPolicy(), + apiClient.createAgentPolicy(TEST_SPACE_1), + apiClient.createAgentPolicy(TEST_SPACE_1), + ]); defaultSpacePolicy1 = _defaultSpacePolicy1; + defaultSpacePolicy2 = _defaultSpacePolicy2; spaceTest1Policy1 = _spaceTest1Policy1; spaceTest1Policy2 = _spaceTest1Policy2; - const [_defaultSpaceAgent1, _defaultSpaceAgent2, _testSpaceAgent1, _testSpaceAgent2] = - await Promise.all([ - createFleetAgent(esClient, defaultSpacePolicy1.item.id, 'default'), - createFleetAgent(esClient, defaultSpacePolicy1.item.id), - createFleetAgent(esClient, spaceTest1Policy1.item.id, TEST_SPACE_1), - createFleetAgent(esClient, spaceTest1Policy2.item.id, TEST_SPACE_1), - ]); - defaultSpaceAgent1 = _defaultSpaceAgent1; - defaultSpaceAgent2 = _defaultSpaceAgent2; - testSpaceAgent1 = _testSpaceAgent1; - testSpaceAgent2 = _testSpaceAgent2; + await createAgents(); }); - describe('GET /agents', () => { + describe('GET /agent', () => { it('should return agents in a specific space', async () => { const agents = await apiClient.getAgents(TEST_SPACE_1); - expect(agents.total).to.eql(2); + expect(agents.total).to.eql(3); const agentIds = agents.items?.map((item) => item.id); expect(agentIds).to.contain(testSpaceAgent1); expect(agentIds).to.contain(testSpaceAgent2); @@ -92,7 +112,7 @@ export default function (providerContext: FtrProviderContext) { }); }); - describe('GET /agents/{id}', () => { + describe('GET /agents/{agentId}', () => { it('should allow to retrieve agent in the same space', async () => { await apiClient.getAgent(testSpaceAgent1, TEST_SPACE_1); }); @@ -110,13 +130,13 @@ export default function (providerContext: FtrProviderContext) { }); }); - describe('PUT /agents/{id}', () => { - it('should allow to update an agent in the same space', async () => { + describe('PUT /agents/{agentId}', () => { + it('should allow updating an agent in the same space', async () => { await apiClient.updateAgent(testSpaceAgent1, { tags: ['foo'] }, TEST_SPACE_1); await apiClient.updateAgent(testSpaceAgent1, { tags: ['tag1'] }, TEST_SPACE_1); }); - it('should not allow to update an agent from a different space from the default space', async () => { + it('should not allow updating an agent from a different space', async () => { let err: Error | undefined; try { await apiClient.updateAgent(testSpaceAgent1, { tags: ['foo'] }); @@ -131,15 +151,15 @@ export default function (providerContext: FtrProviderContext) { describe('DELETE /agents/{id}', () => { it('should allow to delete an agent in the same space', async () => { - const testSpaceAgent3 = await createFleetAgent( + const testSpaceDeleteAgent = await createFleetAgent( esClient, spaceTest1Policy2.item.id, TEST_SPACE_1 ); - await apiClient.deleteAgent(testSpaceAgent3, TEST_SPACE_1); + await apiClient.deleteAgent(testSpaceDeleteAgent, TEST_SPACE_1); }); - it('should not allow to delete an agent from a different space from the default space', async () => { + it('should not allow deleting an agent from a different space', async () => { let err: Error | undefined; try { await apiClient.deleteAgent(testSpaceAgent1); @@ -229,5 +249,333 @@ export default function (providerContext: FtrProviderContext) { expect(agentInTestSpaceTags[testSpaceAgent2]).to.eql(['tag1']); }); }); + + describe('POST /agents/{agentId}/upgrade', () => { + beforeEach(async () => { + await cleanFleetAgents(esClient); + await createAgents(); + }); + + it('should allow upgrading an agent in the same space', async () => { + await makeAgentsUpgradeable(esClient, [testSpaceAgent1], '8.14.0'); + await apiClient.upgradeAgent(testSpaceAgent1, { version: '8.15.0' }, TEST_SPACE_1); + }); + + it('should forbid upgrading an agent from a different space', async () => { + await makeAgentsUpgradeable(esClient, [testSpaceAgent1], '8.14.0'); + const res = await supertest + .post(`/api/fleet/agents/${testSpaceAgent1}/upgrade`) + .set('kbn-xsrf', 'xxxx') + .send({ version: '8.15.0' }) + .expect(404); + expect(res.body.message).to.eql(`Agent ${testSpaceAgent1} not found`); + }); + }); + + describe('POST /agents/bulk_upgrade', () => { + beforeEach(async () => { + await cleanFleetAgents(esClient); + await createAgents(); + }); + + function getAgentStatus(agents: GetAgentsResponse) { + return agents.items?.reduce((acc, item) => { + acc[item.id] = item.status; + return acc; + }, {} as any); + } + + it('should only upgrade agents in the same space when passing a list of agent ids', async () => { + await makeAgentsUpgradeable( + esClient, + [defaultSpaceAgent1, defaultSpaceAgent2, testSpaceAgent1, testSpaceAgent2], + '8.14.0' + ); + + let agents = await apiClient.getAgents(); + let agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [defaultSpaceAgent1]: 'online', + [defaultSpaceAgent2]: 'online', + }); + + agents = await apiClient.getAgents(TEST_SPACE_1); + agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [testSpaceAgent1]: 'online', + [testSpaceAgent2]: 'online', + [testSpaceAgent3]: 'online', + }); + + await apiClient.bulkUpgradeAgents( + { + agents: [defaultSpaceAgent1, testSpaceAgent1], + version: '8.15.0', + skipRateLimitCheck: true, + }, + TEST_SPACE_1 + ); + + agents = await apiClient.getAgents(); + agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [defaultSpaceAgent1]: 'online', + [defaultSpaceAgent2]: 'online', + }); + + agents = await apiClient.getAgents(TEST_SPACE_1); + agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [testSpaceAgent1]: 'updating', + [testSpaceAgent2]: 'online', + [testSpaceAgent3]: 'online', + }); + }); + + it('should only upgrade agents in the same space when passing a kuery', async () => { + await makeAgentsUpgradeable( + esClient, + [defaultSpaceAgent1, defaultSpaceAgent2, testSpaceAgent1, testSpaceAgent2], + '8.14.0' + ); + + let agents = await apiClient.getAgents(); + let agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [defaultSpaceAgent1]: 'online', + [defaultSpaceAgent2]: 'online', + }); + + agents = await apiClient.getAgents(TEST_SPACE_1); + agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [testSpaceAgent1]: 'online', + [testSpaceAgent2]: 'online', + [testSpaceAgent3]: 'online', + }); + + await apiClient.bulkUpgradeAgents( + { + agents: 'status:online', + version: '8.15.0', + skipRateLimitCheck: true, + }, + TEST_SPACE_1 + ); + + agents = await apiClient.getAgents(); + agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [defaultSpaceAgent1]: 'online', + [defaultSpaceAgent2]: 'online', + }); + + agents = await apiClient.getAgents(TEST_SPACE_1); + agentStatus = getAgentStatus(agents); + expect(agentStatus).to.eql({ + [testSpaceAgent1]: 'updating', + [testSpaceAgent2]: 'updating', + [testSpaceAgent3]: 'updating', + }); + }); + }); + + describe('POST /agents/{agentId}/reassign', () => { + beforeEach(async () => { + await cleanFleetAgents(esClient); + await createAgents(); + }); + it('should allow reassigning an agent in the current space to a policy in the current space', async () => { + let agent = await apiClient.getAgent(defaultSpaceAgent1); + expect(agent.item.policy_id).to.eql(defaultSpacePolicy1.item.id); + await apiClient.reassignAgent(defaultSpaceAgent1, defaultSpacePolicy2.item.id); + agent = await apiClient.getAgent(defaultSpaceAgent1); + expect(agent.item.policy_id).to.eql(defaultSpacePolicy2.item.id); + + agent = await apiClient.getAgent(testSpaceAgent1, TEST_SPACE_1); + expect(agent.item.policy_id).to.eql(spaceTest1Policy1.item.id); + await apiClient.reassignAgent(testSpaceAgent1, spaceTest1Policy2.item.id, TEST_SPACE_1); + agent = await apiClient.getAgent(testSpaceAgent1, TEST_SPACE_1); + expect(agent.item.policy_id).to.eql(spaceTest1Policy2.item.id); + + await apiClient.reassignAgent(defaultSpaceAgent1, defaultSpacePolicy1.item.id); + await apiClient.reassignAgent(testSpaceAgent1, spaceTest1Policy1.item.id, TEST_SPACE_1); + }); + + it('should not allow reassigning an agent in a different space', async () => { + let err: Error | undefined; + try { + await apiClient.reassignAgent(testSpaceAgent1, defaultSpacePolicy2.item.id); + } catch (_err) { + err = _err; + } + + expect(err).to.be.an(Error); + expect(err?.message).to.match(/404 "Not Found"/); + }); + + it('should not allow reassigning an agent in the current space to a policy in a different space', async () => { + let err: Error | undefined; + try { + await apiClient.reassignAgent(defaultSpaceAgent1, spaceTest1Policy2.item.id); + } catch (_err) { + err = _err; + } + + expect(err).to.be.an(Error); + expect(err?.message).to.match(/404 "Not Found"/); + }); + }); + + describe('POST /agents/bulk_reassign', () => { + beforeEach(async () => { + await cleanFleetAgents(esClient); + await createAgents(); + }); + function getAgentPolicyIds(agents: GetAgentsResponse) { + return agents.items?.reduce((acc, item) => { + acc[item.id] = item.policy_id; + return acc; + }, {} as any); + } + + it('should return 404 if the policy is in another space', async () => { + let err: Error | undefined; + try { + await apiClient.bulkReassignAgents({ + agents: [defaultSpaceAgent1, testSpaceAgent1], + policy_id: spaceTest1Policy2.item.id, + }); + } catch (_err) { + err = _err; + } + + expect(err).to.be.an(Error); + expect(err?.message).to.match(/404 "Not Found"/); + }); + + it('should only reassign agents in the same space when passing a list of agent ids', async () => { + let agent = await apiClient.getAgent(defaultSpaceAgent1); + expect(agent.item.policy_id).to.eql(defaultSpacePolicy1.item.id); + agent = await apiClient.getAgent(testSpaceAgent1, TEST_SPACE_1); + expect(agent.item.policy_id).to.eql(spaceTest1Policy1.item.id); + + await apiClient.bulkReassignAgents( + { + agents: [defaultSpaceAgent1, testSpaceAgent1], + policy_id: spaceTest1Policy2.item.id, + }, + TEST_SPACE_1 + ); + + agent = await apiClient.getAgent(defaultSpaceAgent1); + expect(agent.item.policy_id).to.eql(defaultSpacePolicy1.item.id); + agent = await apiClient.getAgent(testSpaceAgent1, TEST_SPACE_1); + expect(agent.item.policy_id).to.eql(spaceTest1Policy2.item.id); + + await apiClient.reassignAgent(testSpaceAgent1, spaceTest1Policy1.item.id, TEST_SPACE_1); + }); + + it('should only reassign agents in the same space when passing a kuery', async () => { + let agents = await apiClient.getAgents(); + let agentPolicyIds = getAgentPolicyIds(agents); + expect(agentPolicyIds).to.eql({ + [defaultSpaceAgent1]: defaultSpacePolicy1.item.id, + [defaultSpaceAgent2]: defaultSpacePolicy2.item.id, + }); + agents = await apiClient.getAgents(TEST_SPACE_1); + agentPolicyIds = getAgentPolicyIds(agents); + expect(agentPolicyIds).to.eql({ + [testSpaceAgent1]: spaceTest1Policy1.item.id, + [testSpaceAgent2]: spaceTest1Policy2.item.id, + [testSpaceAgent3]: spaceTest1Policy1.item.id, + }); + + await apiClient.bulkReassignAgents( + { + agents: '*', + policy_id: spaceTest1Policy2.item.id, + }, + TEST_SPACE_1 + ); + + agents = await apiClient.getAgents(); + agentPolicyIds = getAgentPolicyIds(agents); + expect(agentPolicyIds).to.eql({ + [defaultSpaceAgent1]: defaultSpacePolicy1.item.id, + [defaultSpaceAgent2]: defaultSpacePolicy2.item.id, + }); + agents = await apiClient.getAgents(TEST_SPACE_1); + agentPolicyIds = getAgentPolicyIds(agents); + expect(agentPolicyIds).to.eql({ + [testSpaceAgent1]: spaceTest1Policy2.item.id, + [testSpaceAgent2]: spaceTest1Policy2.item.id, + [testSpaceAgent3]: spaceTest1Policy2.item.id, + }); + + await apiClient.reassignAgent(testSpaceAgent1, spaceTest1Policy1.item.id, TEST_SPACE_1); + await apiClient.reassignAgent(testSpaceAgent2, spaceTest1Policy1.item.id, TEST_SPACE_1); + }); + + it('should reassign agents in the same space by kuery in batches', async () => { + let agents = await apiClient.getAgents(); + let agentPolicyIds = getAgentPolicyIds(agents); + expect(agentPolicyIds).to.eql({ + [defaultSpaceAgent1]: defaultSpacePolicy1.item.id, + [defaultSpaceAgent2]: defaultSpacePolicy2.item.id, + }); + agents = await apiClient.getAgents(TEST_SPACE_1); + agentPolicyIds = getAgentPolicyIds(agents); + expect(agentPolicyIds).to.eql({ + [testSpaceAgent1]: spaceTest1Policy1.item.id, + [testSpaceAgent2]: spaceTest1Policy2.item.id, + [testSpaceAgent3]: spaceTest1Policy1.item.id, + }); + + const res = await apiClient.bulkReassignAgents( + { + agents: `not fleet-agents.policy_id:"${spaceTest1Policy2.item.id}"`, + policy_id: spaceTest1Policy2.item.id, + batchSize: 1, + }, + TEST_SPACE_1 + ); + + const verifyActionResult = async () => { + const { body: result } = await supertest + .get(`/s/${TEST_SPACE_1}/api/fleet/agents`) + .set('kbn-xsrf', 'xxx'); + expect(result.total).to.eql(3); + result.items.forEach((agent: any) => { + expect(agent.policy_id).to.eql(spaceTest1Policy2.item.id); + }); + }; + + await new Promise((resolve, reject) => { + let attempts = 0; + const intervalId = setInterval(async () => { + if (attempts > 20) { + clearInterval(intervalId); + reject(new Error('action timed out')); + } + ++attempts; + const { + body: { items: actionStatuses }, + } = await supertest + .get(`/s/${TEST_SPACE_1}/api/fleet/agents/action_status`) + .set('kbn-xsrf', 'xxx'); + + const action = actionStatuses.find((a: any) => a.actionId === res.actionId); + if (action && action.nbAgentsActioned === action.nbAgentsActionCreated) { + clearInterval(intervalId); + await verifyActionResult(); + resolve({}); + } + }, 1000); + }).catch((e) => { + throw e; + }); + }); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts index 58f372ac0d7e..0695dd8868d4 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts @@ -260,6 +260,38 @@ export class SpaceTestApiClient { return res; } + async reassignAgent(agentId: string, policyId: string, spaceId?: string) { + await this.supertest + .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/${agentId}/reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: policyId, + }) + .expect(200); + } + async bulkReassignAgents(data: any, spaceId?: string) { + const { body: res } = await this.supertest + .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxxx') + .send(data) + .expect(200); + + return res; + } + async upgradeAgent(agentId: string, data: any, spaceId?: string) { + await this.supertest + .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/${agentId}/upgrade`) + .set('kbn-xsrf', 'xxxx') + .send(data) + .expect(200); + } + async bulkUpgradeAgents(data: any, spaceId?: string) { + await this.supertest + .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxxx') + .send(data) + .expect(200); + } async bulkUpdateAgentTags(data: any, spaceId?: string) { const { body: res } = await this.supertest .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/bulk_update_agent_tags`) @@ -366,7 +398,6 @@ export class SpaceTestApiClient { return res; } - async postNewAgentAction(agentId: string, spaceId?: string): Promise { const { body: res } = await this.supertest .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/${agentId}/actions`) @@ -376,6 +407,14 @@ export class SpaceTestApiClient { return res; } + + async cancelAction(actionId: string, spaceId?: string): Promise { + const { body: res } = await this.supertest + .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/actions/${actionId}/cancel`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + return res; + } // Enable space awareness async postEnableSpaceAwareness(spaceId?: string): Promise { const { body: res } = await this.supertest diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/helpers.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/helpers.ts index c54291dc588a..a82bf55c352a 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/helpers.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/helpers.ts @@ -15,6 +15,7 @@ import { AGENT_POLICY_INDEX, } from '@kbn/fleet-plugin/common'; import { ENROLLMENT_API_KEYS_INDEX } from '@kbn/fleet-plugin/common/constants'; +import { asyncForEach } from '@kbn/std'; const ES_INDEX_OPTIONS = { headers: { 'X-elastic-product-origin': 'fleet' } }; @@ -50,6 +51,15 @@ export async function cleanFleetIndices(esClient: Client) { ]); } +export async function cleanFleetAgents(esClient: Client) { + await esClient.deleteByQuery({ + index: AGENTS_INDEX, + q: '*', + ignore_unavailable: true, + refresh: true, + }); +} + export async function cleanFleetActionIndices(esClient: Client) { try { await Promise.all([ @@ -78,11 +88,7 @@ export async function cleanFleetActionIndices(esClient: Client) { } } -export const createFleetAgent = async ( - esClient: Client, - agentPolicyId: string, - spaceId?: string -) => { +export async function createFleetAgent(esClient: Client, agentPolicyId: string, spaceId?: string) { const agentResponse = await esClient.index({ index: '.fleet-agents', refresh: true, @@ -106,4 +112,19 @@ export const createFleetAgent = async ( }); return agentResponse._id; -}; +} + +export async function makeAgentsUpgradeable(esClient: Client, agentIds: string[], version: string) { + await asyncForEach(agentIds, async (agentId) => { + await esClient.update({ + id: agentId, + refresh: 'wait_for', + index: AGENTS_INDEX, + body: { + doc: { + local_metadata: { elastic: { agent: { upgradeable: true, version } } }, + }, + }, + }); + }); +} From c2933dee94339f5eca939802f5c1f4a9326c7a70 Mon Sep 17 00:00:00 2001 From: seanrathier Date: Mon, 19 Aug 2024 10:22:45 -0400 Subject: [PATCH 15/25] [Cloud Security] [Agentless] [Serverless] Enable Serverless projects to transition to using Agentless API solution in Kibana (#190371) --- .../ftr_security_serverless_configs.yml | 1 + .../package_policy_input_panel.test.tsx | 8 +- .../hooks/setup_technology.test.ts | 78 +++++++-------- .../hooks/setup_technology.ts | 29 +++--- .../routes/package_policy/utils/index.ts | 4 +- .../services/agents/agentless_agent.test.ts | 2 +- .../server/services/agents/agentless_agent.ts | 8 +- .../server/services/preconfiguration.test.ts | 1 + .../fleet/server/services/preconfiguration.ts | 4 +- .../server/services/utils/agentless.test.ts | 52 +++++++--- .../fleet/server/services/utils/agentless.ts | 18 ++-- ...ig.cloud_security_posture.agentless_api.ts | 34 +++++++ .../agentless_api/create_agent.ts | 97 +++++++++++++++++++ .../agentless_api/index.ts | 15 +++ .../agentless_api/mock_agentless_api.ts | 28 ++++++ 15 files changed, 289 insertions(+), 90 deletions(-) create mode 100644 x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless_api.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/index.ts create mode 100644 x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/mock_agentless_api.ts diff --git a/.buildkite/ftr_security_serverless_configs.yml b/.buildkite/ftr_security_serverless_configs.yml index 4c3b037ce9f8..9f54a402a8bb 100644 --- a/.buildkite/ftr_security_serverless_configs.yml +++ b/.buildkite/ftr_security_serverless_configs.yml @@ -35,6 +35,7 @@ enabled: - x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.basic.ts - x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.essentials.ts - x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless.ts + - x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless_api.ts - x-pack/test_serverless/functional/test_suites/security/config.saved_objects_management.ts - x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts - x-pack/test_serverless/functional/test_suites/security/common_configs/config.group1.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx index c889dc862bf9..5accdf37e95e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_panel.test.tsx @@ -363,8 +363,8 @@ describe('PackagePolicyInputPanel', () => { isAgentlessPackagePolicy: jest.fn(), isAgentlessAgentPolicy: jest.fn(), isAgentlessIntegration: jest.fn(), - isAgentlessCloudEnabled: true, - isAgentlessServerlessEnabled: false, + isAgentlessApiEnabled: true, + isDefaultAgentlessPolicyEnabled: false, }); }); @@ -398,8 +398,8 @@ describe('PackagePolicyInputPanel', () => { isAgentlessPackagePolicy: jest.fn(), isAgentlessAgentPolicy: jest.fn(), isAgentlessIntegration: jest.fn(), - isAgentlessCloudEnabled: true, - isAgentlessServerlessEnabled: false, + isAgentlessApiEnabled: true, + isDefaultAgentlessPolicyEnabled: false, }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts index f32ea1cd007c..be7884aad753 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts @@ -49,37 +49,18 @@ describe('useAgentless', () => { jest.clearAllMocks(); }); - it('should should not return return isAgentless when agentless is not enabled', () => { + it('should not return isAgentless when agentless is not enabled', () => { const { result } = renderHook(() => useAgentless()); expect(result.current.isAgentlessEnabled).toBeFalsy(); - expect(result.current.isAgentlessCloudEnabled).toBeFalsy(); - expect(result.current.isAgentlessServerlessEnabled).toBeFalsy(); - }); - it('should should return agentlessAPIUrl when agentless config is set', () => { - const agentlessAPIUrl = 'https://agentless.api.url'; - (useConfig as MockFn).mockReturnValue({ - agentless: { - api: { - url: agentlessAPIUrl, - }, - }, - } as any); - - const { result } = renderHook(() => useAgentless()); - - expect(result.current.isAgentlessEnabled).toBeFalsy(); - expect(result.current.isAgentlessCloudEnabled).toBeFalsy(); - expect(result.current.isAgentlessServerlessEnabled).toBeFalsy(); + expect(result.current.isAgentlessApiEnabled).toBeFalsy(); + expect(result.current.isDefaultAgentlessPolicyEnabled).toBeFalsy(); }); - it('should return isAgentlessEnabled as falsy if agentlessAPIUrl and experimental feature agentless is truthy without cloud or serverless', () => { - const agentlessAPIUrl = 'https://agentless.api.url'; + it('should return isAgentlessEnabled as falsy if agentless.enabled is true and experimental feature agentless is truthy without cloud or serverless', () => { (useConfig as MockFn).mockReturnValue({ agentless: { - api: { - url: agentlessAPIUrl, - }, + enabled: true, }, } as any); @@ -90,18 +71,14 @@ describe('useAgentless', () => { const { result } = renderHook(() => useAgentless()); expect(result.current.isAgentlessEnabled).toBeFalsy(); - expect(result.current.isAgentlessCloudEnabled).toBeFalsy(); - expect(result.current.isAgentlessServerlessEnabled).toBeFalsy(); + expect(result.current.isAgentlessApiEnabled).toBeFalsy(); + expect(result.current.isDefaultAgentlessPolicyEnabled).toBeFalsy(); }); - it('should return isAgentlessEnabled and isAgentlessCloudEnabled as truthy with isCloudEnabled', () => { - const agentlessAPIUrl = 'https://agentless.api.url'; + it('should return isAgentlessEnabled and isAgentlessApiEnabled as truthy with isCloudEnabled', () => { (useConfig as MockFn).mockReturnValue({ agentless: { enabled: true, - api: { - url: agentlessAPIUrl, - }, }, } as any); @@ -115,19 +92,10 @@ describe('useAgentless', () => { const { result } = renderHook(() => useAgentless()); expect(result.current.isAgentlessEnabled).toBeTruthy(); - expect(result.current.isAgentlessCloudEnabled).toBeTruthy(); - expect(result.current.isAgentlessServerlessEnabled).toBeFalsy(); + expect(result.current.isAgentlessApiEnabled).toBeTruthy(); + expect(result.current.isDefaultAgentlessPolicyEnabled).toBeFalsy(); }); - it('should return isAgentlessEnabled and isAgentlessServerlessEnabled as truthy with isServerlessEnabled', () => { - const agentlessAPIUrl = 'https://agentless.api.url'; - (useConfig as MockFn).mockReturnValue({ - agentless: { - api: { - url: agentlessAPIUrl, - }, - }, - } as any); - + it('should return isAgentlessEnabled and isDefaultAgentlessPolicyEnabled as truthy with isServerlessEnabled and experimental feature agentless is truthy', () => { mockedExperimentalFeaturesService.get.mockReturnValue({ agentless: true, } as any); @@ -142,8 +110,27 @@ describe('useAgentless', () => { const { result } = renderHook(() => useAgentless()); expect(result.current.isAgentlessEnabled).toBeTruthy(); - expect(result.current.isAgentlessCloudEnabled).toBeFalsy(); - expect(result.current.isAgentlessServerlessEnabled).toBeTruthy(); + expect(result.current.isAgentlessApiEnabled).toBeFalsy(); + expect(result.current.isDefaultAgentlessPolicyEnabled).toBeTruthy(); + }); + + it('should return isAgentlessEnabled as falsy and isDefaultAgentlessPolicyEnabled as falsy with isServerlessEnabled and experimental feature agentless is falsy', () => { + mockedExperimentalFeaturesService.get.mockReturnValue({ + agentless: false, + } as any); + + (useStartServices as MockFn).mockReturnValue({ + cloud: { + isServerlessEnabled: true, + isCloudEnabled: false, + }, + }); + + const { result } = renderHook(() => useAgentless()); + + expect(result.current.isAgentlessEnabled).toBeFalsy(); + expect(result.current.isAgentlessApiEnabled).toBeFalsy(); + expect(result.current.isDefaultAgentlessPolicyEnabled).toBeFalsy(); }); }); @@ -224,6 +211,7 @@ describe('useSetupTechnology', () => { it('should set agentless setup technology if agent policy supports agentless in edit page', async () => { (useConfig as MockFn).mockReturnValue({ agentless: { + enabled: true, api: { url: 'https://agentless.api.url', }, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts index 7fc159a2d434..5cafedee7db7 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts @@ -29,10 +29,11 @@ export const useAgentless = () => { const isServerless = !!cloud?.isServerlessEnabled; const isCloud = !!cloud?.isCloudEnabled; - const isAgentlessCloudEnabled = isCloud && !!config.agentless?.enabled; - const isAgentlessServerlessEnabled = isServerless && agentlessExperimentalFeatureEnabled; + const isAgentlessApiEnabled = (isCloud || isServerless) && config.agentless?.enabled; + const isDefaultAgentlessPolicyEnabled = + !isAgentlessApiEnabled && isServerless && agentlessExperimentalFeatureEnabled; - const isAgentlessEnabled = isAgentlessCloudEnabled || isAgentlessServerlessEnabled; + const isAgentlessEnabled = isAgentlessApiEnabled || isDefaultAgentlessPolicyEnabled; const isAgentlessAgentPolicy = (agentPolicy: AgentPolicy | undefined) => { if (!agentPolicy) return false; @@ -62,8 +63,8 @@ export const useAgentless = () => { return isAgentlessEnabled && packagePolicy.policy_ids.includes(AGENTLESS_POLICY_ID); }; return { - isAgentlessCloudEnabled, - isAgentlessServerlessEnabled, + isAgentlessApiEnabled, + isDefaultAgentlessPolicyEnabled, isAgentlessEnabled, isAgentlessAgentPolicy, isAgentlessIntegration, @@ -90,7 +91,7 @@ export function useSetupTechnology({ isEditPage?: boolean; agentPolicies?: AgentPolicy[]; }) { - const { isAgentlessEnabled, isAgentlessCloudEnabled, isAgentlessServerlessEnabled } = + const { isAgentlessEnabled, isAgentlessApiEnabled, isDefaultAgentlessPolicyEnabled } = useAgentless(); // this is a placeholder for the new agent-BASED policy that will be used when the user switches from agentless to agent-based and back @@ -110,7 +111,7 @@ export function useSetupTechnology({ setSelectedSetupTechnology(SetupTechnology.AGENTLESS); return; } - if (isAgentlessCloudEnabled && selectedSetupTechnology === SetupTechnology.AGENTLESS) { + if (isAgentlessApiEnabled && selectedSetupTechnology === SetupTechnology.AGENTLESS) { const nextNewAgentlessPolicy = { ...newAgentlessPolicy, name: getAgentlessAgentPolicyNameFromPackagePolicyName(packagePolicy.name), @@ -122,7 +123,7 @@ export function useSetupTechnology({ } } }, [ - isAgentlessCloudEnabled, + isAgentlessApiEnabled, isEditPage, newAgentlessPolicy, packagePolicy.name, @@ -145,10 +146,10 @@ export function useSetupTechnology({ } }; - if (isAgentlessServerlessEnabled) { + if (isDefaultAgentlessPolicyEnabled) { fetchAgentlessPolicy(); } - }, [isAgentlessServerlessEnabled]); + }, [isDefaultAgentlessPolicyEnabled]); const handleSetupTechnologyChange = useCallback( (setupTechnology: SetupTechnology) => { @@ -157,14 +158,14 @@ export function useSetupTechnology({ } if (setupTechnology === SetupTechnology.AGENTLESS) { - if (isAgentlessCloudEnabled) { + if (isAgentlessApiEnabled) { setNewAgentPolicy(newAgentlessPolicy as NewAgentPolicy); setSelectedPolicyTab(SelectedPolicyTab.NEW); updateAgentPolicies([newAgentlessPolicy] as AgentPolicy[]); } // tech debt: remove this when Serverless uses the Agentless API // https://github.com/elastic/security-team/issues/9781 - if (isAgentlessServerlessEnabled) { + if (isDefaultAgentlessPolicyEnabled) { setNewAgentPolicy(newAgentlessPolicy as AgentPolicy); updateAgentPolicies([newAgentlessPolicy] as AgentPolicy[]); setSelectedPolicyTab(SelectedPolicyTab.EXISTING); @@ -183,8 +184,8 @@ export function useSetupTechnology({ [ isAgentlessEnabled, selectedSetupTechnology, - isAgentlessCloudEnabled, - isAgentlessServerlessEnabled, + isAgentlessApiEnabled, + isDefaultAgentlessPolicyEnabled, setNewAgentPolicy, newAgentlessPolicy, setSelectedPolicyTab, diff --git a/x-pack/plugins/fleet/server/routes/package_policy/utils/index.ts b/x-pack/plugins/fleet/server/routes/package_policy/utils/index.ts index 032dab4a07ac..518d8fc3f74c 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/utils/index.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/utils/index.ts @@ -11,7 +11,7 @@ import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-ser import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { isAgentlessCloudEnabled } from '../../../services/utils/agentless'; +import { isAgentlessApiEnabled } from '../../../services/utils/agentless'; import { getAgentlessAgentPolicyNameFromPackagePolicyName } from '../../../../common/services/agentless_policy_helper'; @@ -65,7 +65,7 @@ export async function renameAgentlessAgentPolicy( packagePolicy: PackagePolicy, name: string ) { - if (!isAgentlessCloudEnabled()) { + if (!isAgentlessApiEnabled()) { return; } // If agentless is enabled for cloud, we need to rename the agent policy diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts index dc057fd962c9..00a1ff4ff61c 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts @@ -111,7 +111,7 @@ describe('Agentless Agent service', () => { namespace: 'default', supports_agentless: true, } as AgentPolicy) - ).rejects.toThrowError(new AgentlessAgentCreateError('Agentless agent not supported')); + ).rejects.toThrowError(new AgentlessAgentCreateError('missing agentless configuration')); }); it('should throw AgentlessAgentCreateError if agentless configuration is not found', async () => { diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts index 21d3a6c8df73..627bdf38b8fe 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts @@ -22,7 +22,7 @@ import { appContextService } from '../app_context'; import { listEnrollmentApiKeys } from '../api_keys'; import { listFleetServerHosts } from '../fleet_server_host'; -import { prependAgentlessApiBasePathToEndpoint } from '../utils/agentless'; +import { prependAgentlessApiBasePathToEndpoint, isAgentlessApiEnabled } from '../utils/agentless'; class AgentlessAgentService { public async createAgentlessAgent( @@ -33,8 +33,10 @@ class AgentlessAgentService { const logger = appContextService.getLogger(); logger.debug(`Creating agentless agent ${agentlessAgentPolicy.id}`); - if (!appContextService.getCloud()?.isCloudEnabled) { - logger.error('Creating agentless agent not supported in non-cloud environments'); + if (!isAgentlessApiEnabled) { + logger.error( + 'Creating agentless agent not supported in non-cloud or non-serverless environments' + ); throw new AgentlessAgentCreateError('Agentless agent not supported'); } if (!agentlessAgentPolicy.supports_agentless) { diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 802edd93e054..36b6d4fdbeb1 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -309,6 +309,7 @@ jest.mock('./app_context', () => ({ getExperimentalFeatures: jest.fn().mockReturnValue({ agentless: false, }), + getConfig: jest.fn(), getInternalUserSOClientForSpaceId: jest.fn(), }, })); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index b12adbe4a243..853961f2fd77 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -43,7 +43,7 @@ import { type InputsOverride, packagePolicyService } from './package_policy'; import { preconfigurePackageInputs } from './package_policy'; import { appContextService } from './app_context'; import type { UpgradeManagedPackagePoliciesResult } from './managed_package_policies'; -import { isAgentlessServerlessEnabled } from './utils/agentless'; +import { isDefaultAgentlessPolicyEnabled } from './utils/agentless'; interface PreconfigurationResult { policies: Array<{ id: string; updated_at: string }>; @@ -164,7 +164,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( } if ( - !isAgentlessServerlessEnabled() && + !isDefaultAgentlessPolicyEnabled() && preconfiguredAgentPolicy?.supports_agentless !== undefined ) { throw new FleetError( diff --git a/x-pack/plugins/fleet/server/services/utils/agentless.test.ts b/x-pack/plugins/fleet/server/services/utils/agentless.test.ts index 4a1bbdd5f7d8..5bf5116128d9 100644 --- a/x-pack/plugins/fleet/server/services/utils/agentless.test.ts +++ b/x-pack/plugins/fleet/server/services/utils/agentless.test.ts @@ -10,9 +10,9 @@ import { securityMock } from '@kbn/security-plugin/server/mocks'; import { appContextService } from '../app_context'; import { - isAgentlessCloudEnabled, + isAgentlessApiEnabled, isAgentlessEnabled, - isAgentlessServerlessEnabled, + isDefaultAgentlessPolicyEnabled, prependAgentlessApiBasePathToEndpoint, } from './agentless'; @@ -23,9 +23,10 @@ mockedAppContextService.getSecuritySetup.mockImplementation(() => ({ ...securityMock.createSetup(), })); -describe('isAgentlessCloudEnabled', () => { +describe('isAgentlessApiEnabled', () => { afterEach(() => { jest.clearAllMocks(); + mockedAppContextService.getConfig.mockReset(); }); it('should return false if cloud is not enabled', () => { jest.spyOn(appContextService, 'getConfig').mockReturnValue({ @@ -35,7 +36,7 @@ describe('isAgentlessCloudEnabled', () => { } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: false } as any); - expect(isAgentlessCloudEnabled()).toBe(false); + expect(isAgentlessApiEnabled()).toBe(false); }); it('should return false if cloud is enabled but agentless is not', () => { @@ -46,7 +47,7 @@ describe('isAgentlessCloudEnabled', () => { } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - expect(isAgentlessCloudEnabled()).toBe(false); + expect(isAgentlessApiEnabled()).toBe(false); }); it('should return true if cloud is enabled and agentless is enabled', () => { @@ -57,13 +58,14 @@ describe('isAgentlessCloudEnabled', () => { } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); - expect(isAgentlessCloudEnabled()).toBe(true); + expect(isAgentlessApiEnabled()).toBe(true); }); }); -describe('isAgentlessServerlessEnabled', () => { +describe('isDefaultAgentlessPolicyEnabled', () => { afterEach(() => { jest.clearAllMocks(); + mockedAppContextService.getConfig.mockReset(); }); it('should return false if serverless is not enabled', () => { @@ -74,7 +76,7 @@ describe('isAgentlessServerlessEnabled', () => { .spyOn(appContextService, 'getCloud') .mockReturnValue({ isServerlessEnabled: false } as any); - expect(isAgentlessServerlessEnabled()).toBe(false); + expect(isDefaultAgentlessPolicyEnabled()).toBe(false); }); it('should return false if serverless is enabled but agentless is not', () => { @@ -83,7 +85,7 @@ describe('isAgentlessServerlessEnabled', () => { .mockReturnValue({ agentless: false } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any); - expect(isAgentlessServerlessEnabled()).toBe(false); + expect(isDefaultAgentlessPolicyEnabled()).toBe(false); }); it('should return true if serverless is enabled and agentless is enabled', () => { @@ -92,13 +94,14 @@ describe('isAgentlessServerlessEnabled', () => { .mockReturnValue({ agentless: true } as any); jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any); - expect(isAgentlessServerlessEnabled()).toBe(true); + expect(isDefaultAgentlessPolicyEnabled()).toBe(true); }); }); describe('isAgentlessEnabled', () => { afterEach(() => { jest.clearAllMocks(); + mockedAppContextService.getConfig.mockReset(); }); it('should return false if cloud and serverless are not enabled', () => { @@ -138,8 +141,8 @@ describe('isAgentlessEnabled', () => { it('should return true if cloud is enabled and agentless is enabled', () => { jest - .spyOn(appContextService, 'getExperimentalFeatures') - .mockReturnValue({ agentless: true } as any); + .spyOn(appContextService, 'getConfig') + .mockReturnValue({ agentless: { enabled: true } } as any); jest .spyOn(appContextService, 'getCloud') .mockReturnValue({ isCloudEnabled: true, isServerlessEnabled: false } as any); @@ -163,7 +166,10 @@ describe('prependAgentlessApiBasePathToEndpoint', () => { jest.clearAllMocks(); }); - it('should prepend the agentless api base path to the endpoint', () => { + it('should prepend the agentless api base path to the endpoint with ess if in cloud', () => { + jest + .spyOn(appContextService, 'getCloud') + .mockReturnValue({ isCloudEnabled: true, isServerlessEnabled: false } as any); const agentlessConfig = { api: { url: 'https://agentless-api.com', @@ -176,7 +182,27 @@ describe('prependAgentlessApiBasePathToEndpoint', () => { ); }); + it('should prepend the agentless api base path to the endpoint with serverless if in serverless', () => { + jest + .spyOn(appContextService, 'getCloud') + .mockReturnValue({ isCloudEnabled: false, isServerlessEnabled: true } as any); + const agentlessConfig = { + api: { + url: 'https://agentless-api.com', + }, + } as any; + const endpoint = '/deployments'; + + expect(prependAgentlessApiBasePathToEndpoint(agentlessConfig, endpoint)).toBe( + 'https://agentless-api.com/api/v1/serverless/deployments' + ); + }); + it('should prepend the agentless api base path to the endpoint with a dynamic path', () => { + jest + .spyOn(appContextService, 'getCloud') + .mockReturnValue({ isCloudEnabled: true, isServerlessEnabled: false } as any); + const agentlessConfig = { api: { url: 'https://agentless-api.com', diff --git a/x-pack/plugins/fleet/server/services/utils/agentless.ts b/x-pack/plugins/fleet/server/services/utils/agentless.ts index d54ea2bb3d00..5c544b1907b2 100644 --- a/x-pack/plugins/fleet/server/services/utils/agentless.ts +++ b/x-pack/plugins/fleet/server/services/utils/agentless.ts @@ -8,21 +8,23 @@ import { appContextService } from '..'; import type { FleetConfigType } from '../../config'; -export const isAgentlessCloudEnabled = () => { +export const isAgentlessApiEnabled = () => { const cloudSetup = appContextService.getCloud(); - return Boolean(cloudSetup?.isCloudEnabled && appContextService.getConfig()?.agentless?.enabled); + const isHosted = cloudSetup?.isCloudEnabled || cloudSetup?.isServerlessEnabled; + return Boolean(isHosted && appContextService.getConfig()?.agentless?.enabled); }; -export const isAgentlessServerlessEnabled = () => { +export const isDefaultAgentlessPolicyEnabled = () => { const cloudSetup = appContextService.getCloud(); return Boolean( cloudSetup?.isServerlessEnabled && appContextService.getExperimentalFeatures().agentless ); }; export const isAgentlessEnabled = () => { - return isAgentlessCloudEnabled() || isAgentlessServerlessEnabled(); + return isAgentlessApiEnabled() || isDefaultAgentlessPolicyEnabled(); }; -const AGENTLESS_API_BASE_PATH = '/api/v1/ess'; +const AGENTLESS_ESS_API_BASE_PATH = '/api/v1/ess'; +const AGENTLESS_SERVERLESS_API_BASE_PATH = '/api/v1/serverless'; type AgentlessApiEndpoints = '/deployments' | `/deployments/${string}`; @@ -30,5 +32,9 @@ export const prependAgentlessApiBasePathToEndpoint = ( agentlessConfig: FleetConfigType['agentless'], endpoint: AgentlessApiEndpoints ) => { - return `${agentlessConfig.api.url}${AGENTLESS_API_BASE_PATH}${endpoint}`; + const cloudSetup = appContextService.getCloud(); + const endpointPrefix = cloudSetup?.isServerlessEnabled + ? AGENTLESS_SERVERLESS_API_BASE_PATH + : AGENTLESS_ESS_API_BASE_PATH; + return `${agentlessConfig.api.url}${endpointPrefix}${endpoint}`; }; diff --git a/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless_api.ts b/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless_api.ts new file mode 100644 index 000000000000..0f37f224197e --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/config.cloud_security_posture.agentless_api.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CLOUD_CREDENTIALS_PACKAGE_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import { createTestConfig } from '../../config.base'; + +export default createTestConfig({ + serverlessProject: 'security', + junit: { + reportName: 'Serverless Security Cloud Security Agentless API Onboarding Functional Tests', + }, + kbnServerArgs: [ + `--xpack.fleet.packages.0.name=cloud_security_posture`, + `--xpack.fleet.packages.0.version=${CLOUD_CREDENTIALS_PACKAGE_VERSION}`, + + `--xpack.fleet.agents.fleet_server.hosts=["https://ftr.kibana:8220"]`, + `--xpack.fleet.internal.fleetServerStandalone=true`, + + // Agentless Configuration based on Serverless Security Dev Yaml - config/serverless.security.dev.yml + `--xpack.fleet.agentless.enabled=true`, + `--xpack.fleet.agentless.api.url=http://localhost:8089`, + `--xpack.fleet.agentless.api.tls.certificate=${KBN_CERT_PATH}`, + `--xpack.fleet.agentless.api.tls.key=${KBN_KEY_PATH}`, + `--xpack.fleet.agentless.api.tls.ca=${CA_CERT_PATH}`, + `--xpack.cloud.serverless.project_id=some_fake_project_id`, + ], + // load tests in the index file + testFiles: [require.resolve('./ftr/cloud_security_posture/agentless_api')], +}); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts new file mode 100644 index 000000000000..d2d797a5c107 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts @@ -0,0 +1,97 @@ +/* + * 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 { CLOUD_CREDENTIALS_PACKAGE_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; +import * as http from 'http'; +import expect from '@kbn/expect'; +import { setupMockServer } from './mock_agentless_api'; +import type { FtrProviderContext } from '../../../../../ftr_provider_context'; +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const mockAgentlessApiService = setupMockServer(); + const pageObjects = getPageObjects([ + 'svlCommonPage', + 'cspSecurity', + 'security', + 'header', + 'cisAddIntegration', + ]); + + const CIS_AWS_OPTION_TEST_ID = 'cisAwsTestId'; + + const AWS_SINGLE_ACCOUNT_TEST_ID = 'awsSingleTestId'; + + describe('Agentless API Serverless', function () { + let mockApiServer: http.Server; + let cisIntegration: typeof pageObjects.cisAddIntegration; + + before(async () => { + mockApiServer = mockAgentlessApiService.listen(8089); // Start the usage api mock server on port 8089 + await pageObjects.svlCommonPage.loginAsAdmin(); + cisIntegration = pageObjects.cisAddIntegration; + }); + + after(async () => { + mockApiServer.close(); + }); + + it(`should create agentless-agent`, async () => { + const integrationPolicyName = `cloud_security_posture-${new Date().toISOString()}`; + await cisIntegration.navigateToAddIntegrationCspmWithVersionPage( + CLOUD_CREDENTIALS_PACKAGE_VERSION + ); + + await cisIntegration.clickOptionButton(CIS_AWS_OPTION_TEST_ID); + await cisIntegration.clickOptionButton(AWS_SINGLE_ACCOUNT_TEST_ID); + + await cisIntegration.inputIntegrationName(integrationPolicyName); + + await cisIntegration.selectSetupTechnology('agentless'); + await cisIntegration.selectAwsCredentials('direct'); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + await cisIntegration.clickSaveButton(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + await cisIntegration.navigateToIntegrationCspList(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + expect(await cisIntegration.getFirstCspmIntegrationPageIntegration()).to.be( + integrationPolicyName + ); + expect(await cisIntegration.getFirstCspmIntegrationPageAgent()).to.be( + `Agentless policy for ${integrationPolicyName}` + ); + }); + + it(`should create default agent-based agent`, async () => { + const integrationPolicyName = `cloud_security_posture-${new Date().toISOString()}`; + + await cisIntegration.navigateToAddIntegrationCspmWithVersionPage( + CLOUD_CREDENTIALS_PACKAGE_VERSION + ); + + await cisIntegration.clickOptionButton(CIS_AWS_OPTION_TEST_ID); + await cisIntegration.clickOptionButton(AWS_SINGLE_ACCOUNT_TEST_ID); + + await cisIntegration.inputIntegrationName(integrationPolicyName); + + await cisIntegration.clickSaveButton(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + const agentPolicyName = await cisIntegration.getAgentBasedPolicyValue(); + + await cisIntegration.navigateToIntegrationCspList(); + await pageObjects.header.waitUntilLoadingHasFinished(); + + expect(await cisIntegration.getFirstCspmIntegrationPageIntegration()).to.be( + integrationPolicyName + ); + expect(await cisIntegration.getFirstCspmIntegrationPageAgent()).to.be(agentPolicyName); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/index.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/index.ts new file mode 100644 index 000000000000..44aea818827d --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/index.ts @@ -0,0 +1,15 @@ +/* + * 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 '../../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('cloud_security_posture', function () { + this.tags(['cloud_security_posture_agentless']); + loadTestFile(require.resolve('./create_agent')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/mock_agentless_api.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/mock_agentless_api.ts new file mode 100644 index 000000000000..8688db0fc018 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/mock_agentless_api.ts @@ -0,0 +1,28 @@ +/* + * 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 { createServer } from '@mswjs/http-middleware'; + +import { http, HttpResponse, StrictResponse } from 'msw'; + +export const setupMockServer = () => { + const server = createServer(deploymentHandler); + return server; +}; + +interface AgentlessApiResponse { + status: number; +} + +const deploymentHandler = http.post( + 'api/v1/serverless/deployments', + async ({ request }): Promise> => { + return HttpResponse.json({ + status: 200, + }); + } +); From 35c0671414ea173ef982a0df6e0332e44ee3436f Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Mon, 19 Aug 2024 17:00:45 +0200 Subject: [PATCH 16/25] [Security Solution][Entity details] - move prevalence related hooks to flyout folder (#190109) --- .../components/analyzer_preview.test.tsx | 4 +- .../right/components/analyzer_preview.tsx | 4 +- .../analyzer_preview_container.test.tsx | 4 +- .../components/insights_section.test.tsx | 4 +- .../visualizations_section.test.tsx | 4 +- .../right/mocks/mock_analyzer_data.ts | 2 +- .../right/utils/analyzer_helpers.ts | 2 +- ...se_alert_document_analyzer_schema.test.tsx | 102 ++++++++++++ .../use_alert_document_analyzer_schema.ts | 95 +++++++++++ .../hooks/use_alert_prevalence.test.tsx | 151 +++++++++++++++++ .../shared/hooks}/use_alert_prevalence.ts | 77 ++++++--- ...lert_prevalence_from_process_tree.test.tsx | 154 ++++++++++++++++++ .../use_alert_prevalence_from_process_tree.ts | 143 ++++++++-------- ..._fetch_related_alerts_by_ancestry.test.tsx | 4 +- .../use_fetch_related_alerts_by_ancestry.ts | 2 +- ...lated_alerts_by_same_source_event.test.tsx | 4 +- ...tch_related_alerts_by_same_source_event.ts | 4 +- ...e_fetch_related_alerts_by_session.test.tsx | 4 +- .../use_fetch_related_alerts_by_session.ts | 4 +- 19 files changed, 655 insertions(+), 113 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.test.tsx create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.ts create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.test.tsx rename x-pack/plugins/security_solution/public/{common/containers/alerts => flyout/document_details/shared/hooks}/use_alert_prevalence.ts (66%) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx rename x-pack/plugins/security_solution/public/{common/containers/alerts => flyout/document_details/shared/hooks}/use_alert_prevalence_from_process_tree.ts (57%) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx index 67d7438e8bb6..724eaf979d8f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.test.tsx @@ -8,7 +8,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { TestProviders } from '../../../../common/mock'; -import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import { useAlertPrevalenceFromProcessTree } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; import { mockContextValue } from '../../shared/mocks/mock_context'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; @@ -17,7 +17,7 @@ import { AnalyzerPreview } from './analyzer_preview'; import { ANALYZER_PREVIEW_TEST_ID } from './test_ids'; import * as mock from '../mocks/mock_analyzer_data'; -jest.mock('../../../../common/containers/alerts/use_alert_prevalence_from_process_tree', () => ({ +jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree', () => ({ useAlertPrevalenceFromProcessTree: jest.fn(), })); const mockUseAlertPrevalenceFromProcessTree = useAlertPrevalenceFromProcessTree as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx index efae023e0d09..bbdcc4f8e3d6 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview.tsx @@ -13,8 +13,8 @@ import { ANALYZER_PREVIEW_TEST_ID, ANALYZER_PREVIEW_LOADING_TEST_ID } from './te import { getTreeNodes } from '../utils/analyzer_helpers'; import { ANCESTOR_ID, RULE_INDICES } from '../../shared/constants/field_names'; import { useDocumentDetailsContext } from '../../shared/context'; -import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; -import type { StatsNode } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import { useAlertPrevalenceFromProcessTree } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; +import type { StatsNode } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; import { isActiveTimeline } from '../../../../helpers'; import { getField } from '../../shared/utils'; import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx index 5ce6fcebae76..7dae9400358c 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/analyzer_preview_container.test.tsx @@ -13,7 +13,7 @@ import { mockContextValue } from '../../shared/mocks/mock_context'; import { AnalyzerPreviewContainer } from './analyzer_preview_container'; import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; import { ANALYZER_PREVIEW_TEST_ID } from './test_ids'; -import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import { useAlertPrevalenceFromProcessTree } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; import * as mock from '../mocks/mock_analyzer_data'; import { EXPANDABLE_PANEL_CONTENT_TEST_ID, @@ -28,7 +28,7 @@ import { useInvestigateInTimeline } from '../../../../detections/components/aler jest.mock( '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver' ); -jest.mock('../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'); +jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree'); jest.mock( '../../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline' ); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx index eb1af2a74b8d..96dff8150e65 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/insights_section.test.tsx @@ -25,13 +25,13 @@ import { usePrevalence } from '../../shared/hooks/use_prevalence'; import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; import { InsightsSection } from './insights_section'; -import { useAlertPrevalence } from '../../../../common/containers/alerts/use_alert_prevalence'; +import { useAlertPrevalence } from '../../shared/hooks/use_alert_prevalence'; import { useRiskScore } from '../../../../entity_analytics/api/hooks/use_risk_score'; import { useExpandSection } from '../hooks/use_expand_section'; import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; import { useTourContext } from '../../../../common/components/guided_onboarding_tour'; -jest.mock('../../../../common/containers/alerts/use_alert_prevalence'); +jest.mock('../../shared/hooks/use_alert_prevalence'); const mockDispatch = jest.fn(); jest.mock('react-redux', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx index 36fe53aa41de..f204c18f9036 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/visualizations_section.test.tsx @@ -18,7 +18,7 @@ import { VisualizationsSection } from './visualizations_section'; import { mockContextValue } from '../../shared/mocks/mock_context'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; import { DocumentDetailsContext } from '../../shared/context'; -import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import { useAlertPrevalenceFromProcessTree } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; import { useExpandSection } from '../hooks/use_expand_section'; @@ -26,7 +26,7 @@ import { useInvestigateInTimeline } from '../../../../detections/components/aler import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; jest.mock('../hooks/use_expand_section'); -jest.mock('../../../../common/containers/alerts/use_alert_prevalence_from_process_tree', () => ({ +jest.mock('../../shared/hooks/use_alert_prevalence_from_process_tree', () => ({ useAlertPrevalenceFromProcessTree: jest.fn(), })); const mockUseAlertPrevalenceFromProcessTree = useAlertPrevalenceFromProcessTree as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/mocks/mock_analyzer_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/mocks/mock_analyzer_data.ts index fbd7dea83f79..e0d35b796e76 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/mocks/mock_analyzer_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/mocks/mock_analyzer_data.ts @@ -7,7 +7,7 @@ import React from 'react'; import { EuiToken } from '@elastic/eui'; import type { Node } from '@elastic/eui/src/components/tree_view/tree_view'; -import type { StatsNode } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import type { StatsNode } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; export const mockStatsNode: StatsNode = { id: '70e19mhyda', diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/analyzer_helpers.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/analyzer_helpers.ts index 15492f7e4137..5db8665bc3bb 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/analyzer_helpers.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/analyzer_helpers.ts @@ -7,7 +7,7 @@ import React from 'react'; import type { Node } from '@elastic/eui/src/components/tree_view/tree_view'; import { EuiToken } from '@elastic/eui'; -import type { StatsNode } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import type { StatsNode } from '../../shared/hooks/use_alert_prevalence_from_process_tree'; /** * Helper function to recursively create ancestor tree nodes diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.test.tsx new file mode 100644 index 000000000000..3c31720b53f9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.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 type { RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import { useQuery } from '@tanstack/react-query'; +import type { + UseAlertDocumentAnalyzerSchemaParams, + UseAlertDocumentAnalyzerSchemaResult, +} from './use_alert_document_analyzer_schema'; +import { useAlertDocumentAnalyzerSchema } from './use_alert_document_analyzer_schema'; +import { useHttp } from '../../../../common/lib/kibana'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('@tanstack/react-query'); + +describe('useAlertPrevalenceFromProcessTree', () => { + let hookResult: RenderHookResult< + UseAlertDocumentAnalyzerSchemaParams, + UseAlertDocumentAnalyzerSchemaResult + >; + + beforeEach(() => { + (useHttp as jest.Mock).mockReturnValue({ + get: jest.fn(), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return all properties when loading', () => { + (useQuery as jest.Mock).mockReturnValue({ + isLoading: true, + data: [], + }); + + hookResult = renderHook(() => + useAlertDocumentAnalyzerSchema({ + documentId: 'documentId', + indices: [], + }) + ); + + expect(hookResult.result.current.loading).toEqual(true); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.id).toEqual(null); + expect(hookResult.result.current.schema).toEqual(null); + expect(hookResult.result.current.agentId).toEqual(null); + }); + + it('should return all properties with data', () => { + (useQuery as jest.Mock).mockReturnValue({ + isLoading: false, + data: [ + { + schema: {}, + id: 'id', + agentId: 'agentId', + }, + ], + }); + + hookResult = renderHook(() => + useAlertDocumentAnalyzerSchema({ + documentId: 'documentId', + indices: [], + }) + ); + + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.id).toEqual('id'); + expect(hookResult.result.current.schema).toEqual({}); + expect(hookResult.result.current.agentId).toEqual('agentId'); + }); + + it('should return error when no data', () => { + (useQuery as jest.Mock).mockReturnValue({ + isLoading: false, + data: [], + }); + + hookResult = renderHook(() => + useAlertDocumentAnalyzerSchema({ + documentId: 'documentId', + indices: [], + }) + ); + + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(true); + expect(hookResult.result.current.id).toEqual(null); + expect(hookResult.result.current.schema).toEqual(null); + expect(hookResult.result.current.agentId).toEqual(null); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.ts new file mode 100644 index 000000000000..63cf63398bd1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_document_analyzer_schema.ts @@ -0,0 +1,95 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import { useHttp } from '../../../../common/lib/kibana'; + +interface EntityResponse { + id: string; + name: string; + schema: object; + agentId: string; +} + +export interface UseAlertDocumentAnalyzerSchemaParams { + /** + * The document ID of the alert to analyze + */ + documentId: string; + /** + * The indices to search for alerts + */ + indices: string[]; +} + +export interface UseAlertDocumentAnalyzerSchemaResult { + /** + * True if the request is still loading + */ + loading: boolean; + /** + * True if there was an error + */ + error: boolean; + /** + * The id returned by the API + */ + id: string | null; + /** + * The schema returned by the API + */ + schema: object | null; + /** + * The agent ID value returned byt the API + */ + agentId: string | null; +} + +export function useAlertDocumentAnalyzerSchema({ + documentId, + indices, +}: UseAlertDocumentAnalyzerSchemaParams): UseAlertDocumentAnalyzerSchemaResult { + const http = useHttp(); + + const query = useQuery(['getAlertPrevalenceSchema', documentId], () => { + return http.get(`/api/endpoint/resolver/entity`, { + query: { + _id: documentId, + indices, + }, + }); + }); + + if (query.isLoading) { + return { + loading: true, + error: false, + id: null, + schema: null, + agentId: null, + }; + } else if (query.data && query.data.length > 0) { + const { + data: [{ schema, id, agentId }], + } = query; + return { + loading: false, + error: false, + id, + schema, + agentId, + }; + } else { + return { + loading: false, + error: true, + id: null, + schema: null, + agentId: null, + }; + } +} diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.test.tsx new file mode 100644 index 000000000000..231e0e541944 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.test.tsx @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import { ALERT_PREVALENCE_AGG, useAlertPrevalence } from './use_alert_prevalence'; +import type { UseAlertPrevalenceParams, UserAlertPrevalenceResult } from './use_alert_prevalence'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { useQueryAlerts } from '../../../../detections/containers/detection_engine/alerts/use_query'; + +jest.mock('../../../../common/containers/use_global_time'); +jest.mock('../../../../common/hooks/use_selector'); +jest.mock('../../../../detections/containers/detection_engine/alerts/use_query'); + +describe('useAlertPrevalence', () => { + let hookResult: RenderHookResult; + + beforeEach(() => { + (useDeepEqualSelector as jest.Mock).mockReturnValue({ + from: 'from', + to: 'to', + }); + (useGlobalTime as jest.Mock).mockReturnValue({ + from: 'from', + to: 'to', + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return all properties', () => { + (useQueryAlerts as jest.Mock).mockReturnValue({ + loading: true, + data: undefined, + setQuery: jest.fn(), + }); + + hookResult = renderHook(() => + useAlertPrevalence({ + field: 'field', + value: 'value', + indexName: 'index', + isActiveTimelines: true, + includeAlertIds: false, + ignoreTimerange: false, + }) + ); + + expect(hookResult.result.current.loading).toEqual(true); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.alertIds).toEqual(undefined); + expect(hookResult.result.current.count).toEqual(undefined); + }); + + it('should return error true if loading is done and no data', () => { + (useQueryAlerts as jest.Mock).mockReturnValue({ + loading: false, + data: undefined, + setQuery: jest.fn(), + }); + + hookResult = renderHook(() => + useAlertPrevalence({ + field: 'field', + value: 'value', + indexName: 'index', + isActiveTimelines: true, + includeAlertIds: false, + ignoreTimerange: false, + }) + ); + + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(true); + expect(hookResult.result.current.alertIds).toEqual(undefined); + expect(hookResult.result.current.count).toEqual(undefined); + }); + + it('should return correct count from aggregation', () => { + (useQueryAlerts as jest.Mock).mockReturnValue({ + loading: false, + data: { + aggregations: { + [ALERT_PREVALENCE_AGG]: { + buckets: [{ doc_count: 1 }], + }, + }, + hits: { + hits: [], + }, + }, + setQuery: jest.fn(), + }); + + hookResult = renderHook(() => + useAlertPrevalence({ + field: 'field', + value: 'value', + indexName: 'index', + isActiveTimelines: true, + includeAlertIds: false, + ignoreTimerange: false, + }) + ); + + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.alertIds).toEqual([]); + expect(hookResult.result.current.count).toEqual(1); + }); + + it('should return alertIds if includeAlertIds is true', () => { + (useQueryAlerts as jest.Mock).mockReturnValue({ + loading: false, + data: { + aggregations: { + [ALERT_PREVALENCE_AGG]: { + buckets: [{ doc_count: 1 }], + }, + }, + hits: { + hits: [{ _id: 'id' }], + }, + }, + setQuery: jest.fn(), + }); + + hookResult = renderHook(() => + useAlertPrevalence({ + field: 'field', + value: 'value', + indexName: 'index', + isActiveTimelines: true, + includeAlertIds: true, + ignoreTimerange: false, + }) + ); + + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.alertIds).toEqual(['id']); + expect(hookResult.result.current.count).toEqual(1); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.ts similarity index 66% rename from x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts rename to x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.ts index cc3ff5507ec4..a68a462c0ec0 100644 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence.ts @@ -7,41 +7,80 @@ import { useEffect, useMemo, useState } from 'react'; -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../common/constants'; -import { useGlobalTime } from '../use_global_time'; -import type { GenericBuckets } from '../../../../common/search_strategy'; -import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query'; -import { ALERTS_QUERY_NAMES } from '../../../detections/containers/detection_engine/alerts/constants'; -import { useDeepEqualSelector } from '../../hooks/use_selector'; -import { inputsSelectors } from '../../store'; - -const ALERT_PREVALENCE_AGG = 'countOfAlertsWithSameFieldAndValue'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../common/constants'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import type { GenericBuckets } from '../../../../../common/search_strategy'; +import { useQueryAlerts } from '../../../../detections/containers/detection_engine/alerts/use_query'; +import { ALERTS_QUERY_NAMES } from '../../../../detections/containers/detection_engine/alerts/constants'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { inputsSelectors } from '../../../../common/store'; + +export const ALERT_PREVALENCE_AGG = 'countOfAlertsWithSameFieldAndValue'; +interface AlertPrevalenceAggregation { + [ALERT_PREVALENCE_AGG]: { + buckets: GenericBuckets[]; + }; +} -interface UseAlertPrevalenceOptions { +export interface UseAlertPrevalenceParams { + /** + * The field to search for + */ field: string; + /** + * The value to search for + */ value: string | string[] | undefined | null; + /** + * The index to search in + */ + indexName: string | null; + /** + * Whether to use the timeline time or the global time + */ isActiveTimelines: boolean; - signalIndexName: string | null; + /** + * Whether to include the alert ids in the response + */ includeAlertIds?: boolean; + /** + * Whether to ignore the timeline time and use the global time + */ ignoreTimerange?: boolean; } -interface UserAlertPrevalenceResult { +export interface UserAlertPrevalenceResult { + /** + * Whether the query is loading + */ loading: boolean; + /** + * The count of the prevalence aggregation + */ count: undefined | number; + /** + * Whether there was an error with the query + */ error: boolean; + /** + * The alert ids sorted by timestamp + */ alertIds?: string[]; } -// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 +/** + * Hook to get the prevalence of alerts with the field and value pair. + * By default, includeAlertIds is false, which means the call only returns the aggregation and not all the alerts themselves. If includeAlertIds is true, it will also return the alertIds sorted by timestamp. + * By default, includeAlertIds is false, which means we're fetching with the global time from and to values. If isActiveTimelines is true, we're getting the timeline time. + */ export const useAlertPrevalence = ({ field, value, + indexName, isActiveTimelines, - signalIndexName, includeAlertIds = false, ignoreTimerange = false, -}: UseAlertPrevalenceOptions): UserAlertPrevalenceResult => { +}: UseAlertPrevalenceParams): UserAlertPrevalenceResult => { const timelineTime = useDeepEqualSelector((state) => inputsSelectors.timelineTimeRangeSelector(state) ); @@ -57,7 +96,7 @@ export const useAlertPrevalence = ({ const { loading, data, setQuery } = useQueryAlerts<{ _id: string }, AlertPrevalenceAggregation>({ query: initialQuery, - indexName: signalIndexName, + indexName, queryName: ALERTS_QUERY_NAMES.PREVALENCE, }); @@ -165,9 +204,3 @@ const generateAlertPrevalenceQuery = ( runtime_mappings: {}, }; }; - -export interface AlertPrevalenceAggregation { - [ALERT_PREVALENCE_AGG]: { - buckets: GenericBuckets[]; - }; -} diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx new file mode 100644 index 000000000000..94b7cfa62350 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.test.tsx @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import type { + UseAlertPrevalenceFromProcessTreeParams, + UserAlertPrevalenceFromProcessTreeResult, +} from './use_alert_prevalence_from_process_tree'; +import { useAlertPrevalenceFromProcessTree } from './use_alert_prevalence_from_process_tree'; +import { useHttp } from '../../../../common/lib/kibana'; +import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; +import { useQuery } from '@tanstack/react-query'; +import { useAlertDocumentAnalyzerSchema } from './use_alert_document_analyzer_schema'; +import { mockStatsNode } from '../../right/mocks/mock_analyzer_data'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../timelines/containers/use_timeline_data_filters'); +jest.mock('./use_alert_document_analyzer_schema'); +jest.mock('@tanstack/react-query'); + +describe('useAlertPrevalenceFromProcessTree', () => { + let hookResult: RenderHookResult< + UseAlertPrevalenceFromProcessTreeParams, + UserAlertPrevalenceFromProcessTreeResult + >; + + beforeEach(() => { + (useHttp as jest.Mock).mockReturnValue({ + post: jest.fn(), + }); + (useTimelineDataFilters as jest.Mock).mockReturnValue({ + selectedPatterns: [], + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return all properties when query is loading', () => { + (useQuery as jest.Mock).mockReturnValue({ + isLoading: true, + data: {}, + }); + (useAlertDocumentAnalyzerSchema as jest.Mock).mockReturnValue({ + loading: false, + error: false, + id: null, + schema: null, + agentId: null, + }); + + hookResult = renderHook(() => + useAlertPrevalenceFromProcessTree({ + documentId: 'documentId', + isActiveTimeline: true, + indices: [], + }) + ); + + expect(hookResult.result.current.loading).toEqual(true); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.alertIds).toEqual(undefined); + expect(hookResult.result.current.statsNodes).toEqual(undefined); + }); + + it('should return all properties when analyzer query is loading', () => { + (useQuery as jest.Mock).mockReturnValue({ + isLoading: false, + data: {}, + }); + (useAlertDocumentAnalyzerSchema as jest.Mock).mockReturnValue({ + loading: true, + error: false, + id: null, + schema: null, + agentId: null, + }); + + hookResult = renderHook(() => + useAlertPrevalenceFromProcessTree({ + documentId: 'documentId', + isActiveTimeline: true, + indices: [], + }) + ); + + expect(hookResult.result.current.loading).toEqual(true); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.alertIds).toEqual(undefined); + expect(hookResult.result.current.statsNodes).toEqual(undefined); + }); + + it('should return all properties data exists', () => { + (useQuery as jest.Mock).mockReturnValue({ + isLoading: false, + data: { + alertIds: ['alertIds'], + statsNodes: [mockStatsNode], + }, + }); + (useAlertDocumentAnalyzerSchema as jest.Mock).mockReturnValue({ + loading: false, + error: false, + id: null, + schema: null, + agentId: null, + }); + + hookResult = renderHook(() => + useAlertPrevalenceFromProcessTree({ + documentId: 'documentId', + isActiveTimeline: true, + indices: [], + }) + ); + + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.alertIds).toEqual(['alertIds']); + expect(hookResult.result.current.statsNodes).toEqual([mockStatsNode]); + }); + + it('should return all properties data undefined', () => { + (useQuery as jest.Mock).mockReturnValue({ + isLoading: false, + }); + (useAlertDocumentAnalyzerSchema as jest.Mock).mockReturnValue({ + loading: false, + error: false, + id: null, + schema: null, + agentId: null, + }); + + hookResult = renderHook(() => + useAlertPrevalenceFromProcessTree({ + documentId: 'documentId', + isActiveTimeline: true, + indices: [], + }) + ); + + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(true); + expect(hookResult.result.current.alertIds).toEqual(undefined); + expect(hookResult.result.current.statsNodes).toEqual(undefined); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence_from_process_tree.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.ts similarity index 57% rename from x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence_from_process_tree.ts rename to x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.ts index 4e6747384fe3..f9c27f6e2ccb 100644 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence_from_process_tree.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_alert_prevalence_from_process_tree.ts @@ -4,114 +4,120 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useQuery } from '@tanstack/react-query'; -import { useHttp } from '../../lib/kibana'; -import { useTimelineDataFilters } from '../../../timelines/containers/use_timeline_data_filters'; -export const DETECTIONS_ALERTS_COUNT_ID = 'detections-alerts-count'; +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import { useAlertDocumentAnalyzerSchema } from './use_alert_document_analyzer_schema'; +import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters'; +import { useHttp } from '../../../../common/lib/kibana'; export interface StatsNode { + /** + * The data of the node + */ data: object; + /** + * The ID of the node + */ id: string; + /** + * The name of the node + */ name: string; + /** + * The parent ID of the node + */ parent?: string; stats: { + /** + * The total number of alerts + */ total: number; + /** + * The total number of alerts by category + */ byCategory: { alerts?: number; }; }; } -interface UserAlertPrevalenceFromProcessTreeResult { - loading: boolean; - alertIds: undefined | string[]; - statsNodes: undefined | StatsNode[]; - count?: number; - error: boolean; -} - interface ProcessTreeAlertPrevalenceResponse { + /** + * The alert IDs found in the process tree + */ alertIds: string[] | undefined; + /** + * The stats nodes found in the process tree + */ statsNodes: StatsNode[] | undefined; } -interface EntityResponse { - id: string; - name: string; - schema: object; - agentId: string; +interface TreeResponse { + /** + * The alert IDs found in the process tree + */ + alertIds: string[]; + /** + * The stats nodes found in the process tree + */ + statsNodes: StatsNode[]; } -interface UseAlertPrevalenceFromProcessTree { +export interface UseAlertPrevalenceFromProcessTreeParams { + /** + * The document ID of the alert to analyze + */ documentId: string; + /** + * Whether or not the timeline is active + */ isActiveTimeline: boolean; + /** + * The indices to search for alerts + */ indices: string[]; } -interface UseAlertDocumentAnalyzerSchema { - documentId: string; - indices: string[]; -} - -interface TreeResponse { - statsNodes: StatsNode[]; - alertIds: string[]; -} - -function useAlertDocumentAnalyzerSchema({ documentId, indices }: UseAlertDocumentAnalyzerSchema) { - const http = useHttp(); - const query = useQuery(['getAlertPrevalenceSchema', documentId], () => { - return http.get(`/api/endpoint/resolver/entity`, { - query: { - _id: documentId, - indices, - }, - }); - }); - if (query.isLoading) { - return { - loading: true, - error: false, - id: null, - schema: null, - agentId: null, - }; - } else if (query.data && query.data.length > 0) { - const { - data: [{ schema, id, agentId }], - } = query; - return { - loading: false, - error: false, - id, - schema, - agentId, - }; - } else { - return { - loading: false, - error: true, - id: null, - schema: null, - agentId: null, - }; - } +export interface UserAlertPrevalenceFromProcessTreeResult { + /** + * Whether or not the query is loading + */ + loading: boolean; + /** + * The alert IDs found in the process tree + */ + alertIds: undefined | string[]; + /** + * The stats nodes found in the process tree + */ + statsNodes: undefined | StatsNode[]; + /** + * Whether or not the query errored + */ + error: boolean; } +/** + * Fetches the alert prevalence from the process tree + */ export function useAlertPrevalenceFromProcessTree({ documentId, isActiveTimeline, indices, -}: UseAlertPrevalenceFromProcessTree): UserAlertPrevalenceFromProcessTreeResult { +}: UseAlertPrevalenceFromProcessTreeParams): UserAlertPrevalenceFromProcessTreeResult { const http = useHttp(); const { selectedPatterns } = useTimelineDataFilters(isActiveTimeline); - const alertAndOriginalIndices = [...new Set(selectedPatterns.concat(indices))]; + const alertAndOriginalIndices = useMemo( + () => [...new Set(selectedPatterns.concat(indices))], + [indices, selectedPatterns] + ); const { loading, id, schema, agentId } = useAlertDocumentAnalyzerSchema({ documentId, indices: alertAndOriginalIndices, }); + const query = useQuery( ['getAlertPrevalenceFromProcessTree', id], () => { @@ -129,6 +135,7 @@ export function useAlertPrevalenceFromProcessTree({ }, { enabled: schema !== null && id !== null } ); + if (query.isLoading || loading) { return { loading: true, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.test.tsx index 9291b5e9a0c1..4d65339c6b41 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.test.tsx @@ -12,9 +12,9 @@ import type { UseFetchRelatedAlertsByAncestryResult, } from './use_fetch_related_alerts_by_ancestry'; import { useFetchRelatedAlertsByAncestry } from './use_fetch_related_alerts_by_ancestry'; -import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import { useAlertPrevalenceFromProcessTree } from './use_alert_prevalence_from_process_tree'; -jest.mock('../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'); +jest.mock('./use_alert_prevalence_from_process_tree'); const documentId = 'documentId'; const indices = ['index1']; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.ts index b44349a06eec..826290a3dd3e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_ancestry.ts @@ -6,7 +6,7 @@ */ import { useMemo } from 'react'; -import { useAlertPrevalenceFromProcessTree } from '../../../../common/containers/alerts/use_alert_prevalence_from_process_tree'; +import { useAlertPrevalenceFromProcessTree } from './use_alert_prevalence_from_process_tree'; import { isActiveTimeline } from '../../../../helpers'; export interface UseFetchRelatedAlertsByAncestryParams { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.test.tsx index 4aaab73af129..ff74774068ad 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.test.tsx @@ -12,9 +12,9 @@ import type { UseFetchRelatedAlertsBySameSourceEventResult, } from './use_fetch_related_alerts_by_same_source_event'; import { useFetchRelatedAlertsBySameSourceEvent } from './use_fetch_related_alerts_by_same_source_event'; -import { useAlertPrevalence } from '../../../../common/containers/alerts/use_alert_prevalence'; +import { useAlertPrevalence } from './use_alert_prevalence'; -jest.mock('../../../../common/containers/alerts/use_alert_prevalence'); +jest.mock('./use_alert_prevalence'); const originalEventId = 'originalEventId'; const scopeId = 'scopeId'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.ts index 1946cef3e7de..209bcb0c0405 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_same_source_event.ts @@ -7,7 +7,7 @@ import { useMemo } from 'react'; import { ANCESTOR_ID } from '../constants/field_names'; -import { useAlertPrevalence } from '../../../../common/containers/alerts/use_alert_prevalence'; +import { useAlertPrevalence } from './use_alert_prevalence'; import { isActiveTimeline } from '../../../../helpers'; export interface UseFetchRelatedAlertsBySameSourceEventParams { @@ -50,7 +50,7 @@ export const useFetchRelatedAlertsBySameSourceEvent = ({ field: ANCESTOR_ID, value: originalEventId, isActiveTimelines: isActiveTimeline(scopeId), - signalIndexName: null, + indexName: null, includeAlertIds: true, }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.test.tsx index 6f6f2ea73158..b38ef44178f9 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.test.tsx @@ -13,9 +13,9 @@ import type { UseFetchRelatedAlertsBySessionResult, } from './use_fetch_related_alerts_by_session'; import { useFetchRelatedAlertsBySession } from './use_fetch_related_alerts_by_session'; -import { useAlertPrevalence } from '../../../../common/containers/alerts/use_alert_prevalence'; +import { useAlertPrevalence } from './use_alert_prevalence'; -jest.mock('../../../../common/containers/alerts/use_alert_prevalence'); +jest.mock('./use_alert_prevalence'); const entityId = 'entityId'; const scopeId = 'scopeId'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.ts index 2c70714d07d5..606c3523f60b 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_fetch_related_alerts_by_session.ts @@ -6,7 +6,7 @@ */ import { useMemo } from 'react'; -import { useAlertPrevalence } from '../../../../common/containers/alerts/use_alert_prevalence'; +import { useAlertPrevalence } from './use_alert_prevalence'; import { isActiveTimeline } from '../../../../helpers'; import { ENTRY_LEADER_ENTITY_ID } from '../constants/field_names'; @@ -50,7 +50,7 @@ export const useFetchRelatedAlertsBySession = ({ field: ENTRY_LEADER_ENTITY_ID, value: entityId, isActiveTimelines: isActiveTimeline(scopeId), - signalIndexName: null, + indexName: null, includeAlertIds: true, ignoreTimerange: true, }); From 04503bffe9110196d591d7886f7a62ea7ab61982 Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Mon, 19 Aug 2024 17:01:07 +0200 Subject: [PATCH 17/25] [Security Solution][Entity details] - move osquery, response and investigation guide related hooks and components to flyout folder (#190110) --- .../components/event_details/translations.ts | 32 ---------- .../markdown_editor/plugins/insight/index.tsx | 2 +- .../plugins/osquery/renderer.tsx | 2 +- .../left/components/investigation_guide.tsx | 2 +- .../investigation_guide_view.test.tsx | 2 +- .../components}/investigation_guide_view.tsx | 18 ++++-- .../left/components/response_details.tsx | 2 +- .../hooks/use_response_actions_view.test.ts | 61 +++++++++++++++++++ .../left/hooks/use_response_actions_view.tsx} | 60 ++++++++++-------- .../translations/translations/fr-FR.json | 5 -- .../translations/translations/ja-JP.json | 5 -- .../translations/translations/zh-CN.json | 5 -- 12 files changed, 114 insertions(+), 82 deletions(-) rename x-pack/plugins/security_solution/public/{common/components/event_details => flyout/document_details/left/components}/investigation_guide_view.test.tsx (92%) rename x-pack/plugins/security_solution/public/{common/components/event_details => flyout/document_details/left/components}/investigation_guide_view.tsx (81%) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.test.ts rename x-pack/plugins/security_solution/public/{common/components/event_details/response_actions_view.tsx => flyout/document_details/left/hooks/use_response_actions_view.tsx} (75%) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index d4d129f57ae3..2362284b3f69 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -7,28 +7,10 @@ import { i18n } from '@kbn/i18n'; -export const INVESTIGATION_GUIDE = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.investigationGuide', - { - defaultMessage: 'Investigation guide', - } -); - export const TABLE = i18n.translate('xpack.securitySolution.eventDetails.table', { defaultMessage: 'Table', }); -export const OSQUERY_VIEW = i18n.translate('xpack.securitySolution.eventDetails.osqueryView', { - defaultMessage: 'Osquery Results', -}); - -export const RESPONSE_ACTIONS_VIEW = i18n.translate( - 'xpack.securitySolution.eventDetails.responseActionsView', - { - defaultMessage: 'Response Results', - } -); - export const DESCRIPTION = i18n.translate('xpack.securitySolution.eventDetails.description', { defaultMessage: 'Description', }); @@ -48,20 +30,6 @@ export const RULE_TYPE = i18n.translate('xpack.securitySolution.detections.alert defaultMessage: 'Rule type', }); -export const MULTI_FIELD_TOOLTIP = i18n.translate( - 'xpack.securitySolution.eventDetails.multiFieldTooltipContent', - { - defaultMessage: 'Multi-fields can have multiple values per field', - } -); - -export const MULTI_FIELD_BADGE = i18n.translate( - 'xpack.securitySolution.eventDetails.multiFieldBadge', - { - defaultMessage: 'multi-field', - } -); - export const ACTIONS = i18n.translate('xpack.securitySolution.eventDetails.table.actions', { defaultMessage: 'Actions', }); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index 6eae6b723d54..791bace753ff 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -42,7 +42,7 @@ import { useAppToasts } from '../../../../hooks/use_app_toasts'; import { useKibana } from '../../../../lib/kibana'; import { useInsightQuery } from './use_insight_query'; import { useInsightDataProviders, type Provider } from './use_insight_data_providers'; -import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view'; +import { BasicAlertDataContext } from '../../../../../flyout/document_details/left/components/investigation_guide_view'; import { InvestigateInTimelineButton } from '../../../event_details/table/investigate_in_timeline_button'; import { getTimeRangeSettings, diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/renderer.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/renderer.tsx index 04963e70f9cf..198f64bb2523 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/renderer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/osquery/renderer.tsx @@ -13,7 +13,7 @@ import styled from 'styled-components'; import { EuiButton, EuiToolTip } from '@elastic/eui'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { useUpsellingMessage } from '../../../../hooks/use_upselling'; -import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view'; +import { BasicAlertDataContext } from '../../../../../flyout/document_details/left/components/investigation_guide_view'; import { expandDottedObject } from '../../../../../../common/utils/expand_dotted'; import OsqueryLogo from './osquery_icon/osquery.svg'; import { OsqueryFlyout } from '../../../../../detections/components/osquery/osquery_flyout'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide.tsx index 0bf6ca92b28f..ee1bebdb336c 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useInvestigationGuide } from '../../shared/hooks/use_investigation_guide'; import { useDocumentDetailsContext } from '../../shared/context'; import { INVESTIGATION_GUIDE_TEST_ID, INVESTIGATION_GUIDE_LOADING_TEST_ID } from './test_ids'; -import { InvestigationGuideView } from '../../../../common/components/event_details/investigation_guide_view'; +import { InvestigationGuideView } from './investigation_guide_view'; import { FlyoutLoading } from '../../../shared/components/flyout_loading'; /** diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide_view.test.tsx similarity index 92% rename from x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.test.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide_view.test.tsx index 355ad1f9129d..bc7de71c5641 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide_view.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { InvestigationGuideView } from './investigation_guide_view'; -import type { UseBasicDataFromDetailsDataResult } from '../../../flyout/document_details/shared/hooks/use_basic_data_from_details_data'; +import type { UseBasicDataFromDetailsDataResult } from '../../shared/hooks/use_basic_data_from_details_data'; const defaultProps = { basicData: { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide_view.tsx similarity index 81% rename from x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide_view.tsx index b3015bafe453..3d61c223fd47 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/investigation_guide_view.tsx @@ -8,10 +8,17 @@ import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; import React, { createContext } from 'react'; import styled from 'styled-components'; -import type { UseBasicDataFromDetailsDataResult } from '../../../flyout/document_details/shared/hooks/use_basic_data_from_details_data'; -import * as i18n from './translations'; -import { MarkdownRenderer } from '../markdown_editor'; -import { LineClamp } from '../line_clamp'; +import { i18n } from '@kbn/i18n'; +import type { UseBasicDataFromDetailsDataResult } from '../../shared/hooks/use_basic_data_from_details_data'; +import { LineClamp } from '../../../../common/components/line_clamp'; +import { MarkdownRenderer } from '../../../../common/components/markdown_editor'; + +const INVESTIGATION_GUIDE = i18n.translate( + 'xpack.securitySolution.flyout.left.investigationGuide', + { + defaultMessage: 'Investigation guide', + } +); export const Indent = styled.div` padding: 0 8px; @@ -43,7 +50,6 @@ interface InvestigationGuideViewProps { /** * Investigation guide that shows the markdown text of rule.note */ -// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 const InvestigationGuideViewComponent: React.FC = ({ basicData, ruleNote, @@ -56,7 +62,7 @@ const InvestigationGuideViewComponent: React.FC = ( <> -
{i18n.INVESTIGATION_GUIDE}
+
{INVESTIGATION_GUIDE}
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/response_details.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/response_details.tsx index 5081bdad9c17..a26e63674978 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/response_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/response_details.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n-react'; import { RESPONSE_DETAILS_TEST_ID } from './test_ids'; import { useDocumentDetailsContext } from '../../shared/context'; -import { useResponseActionsView } from '../../../../common/components/event_details/response_actions_view'; +import { useResponseActionsView } from '../hooks/use_response_actions_view'; const ExtendedFlyoutWrapper = styled.div` figure { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.test.ts new file mode 100644 index 000000000000..cafac9f3a0b9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useResponseActionsView } from './use_response_actions_view'; +import { mockSearchHit } from '../../shared/mocks/mock_search_hit'; +import { mockDataAsNestedObject } from '../../shared/mocks/mock_data_as_nested_object'; +import { useGetAutomatedActionList } from '../../../../management/hooks/response_actions/use_get_automated_action_list'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; + +const ecsData = mockDataAsNestedObject; +const rawEventData = mockSearchHit; + +jest.mock('../../../../common/hooks/use_experimental_features'); +jest.mock('../../../../management/hooks/response_actions/use_get_automated_action_list'); + +describe('useResponseActionsView', () => { + it('should return the normal component', () => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + (useGetAutomatedActionList as jest.Mock).mockReturnValue({ + data: [], + isFetched: true, + }); + + const { result } = renderHook(() => + useResponseActionsView({ + ecsData, + rawEventData, + }) + ); + + expect(result.current.id).toEqual('response-actions-results-view'); + expect(result.current.name).toEqual('Response Results'); + expect(result.current.append).toBeDefined(); + expect(result.current.content).toBeDefined(); + }); + + it('returns early return if rawEventData is undefined', () => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + (useGetAutomatedActionList as jest.Mock).mockReturnValue({ + data: [], + isFetched: true, + }); + + const { result } = renderHook(() => + useResponseActionsView({ + ecsData, + rawEventData: undefined, + }) + ); + + expect(result.current.id).toEqual('response-actions-results-view'); + expect(result.current.name).toEqual('Response Results'); + expect(result.current.append).not.toBeDefined(); + expect(result.current.content).toBeDefined(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/response_actions_view.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.tsx similarity index 75% rename from x-pack/plugins/security_solution/public/common/components/event_details/response_actions_view.tsx rename to x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.tsx index 33760b7ab424..b6966b529d3d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/response_actions_view.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_response_actions_view.tsx @@ -5,23 +5,29 @@ * 2.0. */ -import React, { useMemo, useState, useEffect } from 'react'; -import styled from 'styled-components'; +import React, { useState, useEffect } from 'react'; +import styled from '@emotion/styled'; import type { EuiTabbedContentTab } from '@elastic/eui'; import { EuiLink, EuiNotificationBadge, EuiSpacer } from '@elastic/eui'; import type { Ecs } from '@kbn/cases-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; -import { RESPONSE_NO_DATA_TEST_ID } from '../../../flyout/document_details/left/components/test_ids'; -import type { SearchHit } from '../../../../common/search_strategy'; +import { i18n } from '@kbn/i18n'; +import { RESPONSE_NO_DATA_TEST_ID } from '../components/test_ids'; +import type { SearchHit } from '../../../../../common/search_strategy'; import type { ExpandedEventFieldsObject, RawEventData, -} from '../../../../common/types/response_actions'; -import { ResponseActionsResults } from '../response_actions/response_actions_results'; -import { expandDottedObject } from '../../../../common/utils/expand_dotted'; -import { useGetAutomatedActionList } from '../../../management/hooks/response_actions/use_get_automated_action_list'; -import { EventsViewType } from './event_details'; -import * as i18n from './translations'; +} from '../../../../../common/types/response_actions'; +import { ResponseActionsResults } from '../../../../common/components/response_actions/response_actions_results'; +import { expandDottedObject } from '../../../../../common/utils/expand_dotted'; +import { useGetAutomatedActionList } from '../../../../management/hooks/response_actions/use_get_automated_action_list'; + +const RESPONSE_ACTIONS_VIEW = i18n.translate( + 'xpack.securitySolution.flyout.response.responseActionsView', + { + defaultMessage: 'Response Results', + } +); const TabContentWrapper = styled.div` height: 100%; @@ -56,23 +62,29 @@ const EmptyResponseActions = () => { ); }; -// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 +const viewData = { + id: 'response-actions-results-view', + name: RESPONSE_ACTIONS_VIEW, +}; + +export interface UseResponseActionsViewParams { + /** + * An object with top level fields from the ECS object + */ + ecsData?: Ecs | null; + /** + * The actual raw document object + */ + rawEventData: SearchHit | undefined; +} + +/** + * + */ export const useResponseActionsView = ({ rawEventData, ecsData, -}: { - ecsData?: Ecs | null; - rawEventData: SearchHit | undefined; -}): EuiTabbedContentTab | undefined => { - // can not be moved outside of the component, because then EventsViewType throws runtime error regarding not being initialized yet - const viewData = useMemo( - () => ({ - id: EventsViewType.responseActionsView, - 'data-test-subj': 'responseActionsViewTab', - name: i18n.RESPONSE_ACTIONS_VIEW, - }), - [] - ); +}: UseResponseActionsViewParams): EuiTabbedContentTab => { const expandedEventFieldsObject = rawEventData ? (expandDottedObject((rawEventData as RawEventData).fields) as ExpandedEventFieldsObject) : undefined; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 2fa4a66e8b37..cc0f7067d89e 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -35559,7 +35559,6 @@ "xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "Nom de règle", "xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "Données de risque de {riskEntity}", "xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCountTechnicalPreview": "Version d'évaluation technique", - "xpack.securitySolution.alertDetails.overview.investigationGuide": "Guide d'investigation", "xpack.securitySolution.alertDetails.summary.readLess": "Lire moins", "xpack.securitySolution.alertDetails.summary.readMore": "En savoir plus", "xpack.securitySolution.alerts.badge.readOnly.tooltip": "Impossible de mettre à jour les alertes", @@ -38924,14 +38923,10 @@ "xpack.securitySolution.event.summary.threat_indicator.showMatches": "Afficher les {count} alertes de correspondance d'indicateur", "xpack.securitySolution.eventDetails.alertReason": "Raison d'alerte", "xpack.securitySolution.eventDetails.description": "Description", - "xpack.securitySolution.eventDetails.multiFieldBadge": "champ multiple", - "xpack.securitySolution.eventDetails.multiFieldTooltipContent": "Les champs multiples peuvent avoir plusieurs valeurs.", - "xpack.securitySolution.eventDetails.osqueryView": "Résultats Osquery", "xpack.securitySolution.eventDetails.responseActions.endpoint.executed": "a exécuté la commande {command}", "xpack.securitySolution.eventDetails.responseActions.endpoint.failed": "n'a pas pu exécuter la commande {command}", "xpack.securitySolution.eventDetails.responseActions.endpoint.pending": "exécute la commande {command}", "xpack.securitySolution.eventDetails.responseActions.endpoint.tried": "a tenté d'exécuter la commande {command}", - "xpack.securitySolution.eventDetails.responseActionsView": "Résultats de la réponse", "xpack.securitySolution.eventDetails.summaryView": "résumé", "xpack.securitySolution.eventDetails.table": "Tableau", "xpack.securitySolution.eventDetails.table.actions": "Actions", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c26fe66d0393..519c9f2a428a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -35543,7 +35543,6 @@ "xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "ルール名", "xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "{riskEntity}リスクデータ", "xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCountTechnicalPreview": "テクニカルプレビュー", - "xpack.securitySolution.alertDetails.overview.investigationGuide": "調査ガイド", "xpack.securitySolution.alertDetails.summary.readLess": "表示を減らす", "xpack.securitySolution.alertDetails.summary.readMore": "続きを読む", "xpack.securitySolution.alerts.badge.readOnly.tooltip": "アラートを更新できません", @@ -38905,14 +38904,10 @@ "xpack.securitySolution.event.summary.threat_indicator.showMatches": "すべての{count}件のインジケーター一致アラートを表示", "xpack.securitySolution.eventDetails.alertReason": "アラートの理由", "xpack.securitySolution.eventDetails.description": "説明", - "xpack.securitySolution.eventDetails.multiFieldBadge": "複数フィールド", - "xpack.securitySolution.eventDetails.multiFieldTooltipContent": "複数フィールドにはフィールドごとに複数の値を入力できます", - "xpack.securitySolution.eventDetails.osqueryView": "Osquery結果", "xpack.securitySolution.eventDetails.responseActions.endpoint.executed": "{command}コマンドを実行しました", "xpack.securitySolution.eventDetails.responseActions.endpoint.failed": "{command}コマンドを実行できませんでした", "xpack.securitySolution.eventDetails.responseActions.endpoint.pending": "{command}コマンドを実行しています", "xpack.securitySolution.eventDetails.responseActions.endpoint.tried": "{command}コマンドを実行しようとしました", - "xpack.securitySolution.eventDetails.responseActionsView": "対応の結果", "xpack.securitySolution.eventDetails.summaryView": "まとめ", "xpack.securitySolution.eventDetails.table": "表", "xpack.securitySolution.eventDetails.table.actions": "アクション", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c4f2618a499d..328008ebf395 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -35584,7 +35584,6 @@ "xpack.securitySolution.alertCountByRuleByStatus.tooltipTitle": "规则名称", "xpack.securitySolution.alertDetails.overview.hostRiskDataTitle": "{riskEntity}风险数据", "xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCountTechnicalPreview": "技术预览", - "xpack.securitySolution.alertDetails.overview.investigationGuide": "调查指南", "xpack.securitySolution.alertDetails.summary.readLess": "阅读更少内容", "xpack.securitySolution.alertDetails.summary.readMore": "阅读更多内容", "xpack.securitySolution.alerts.badge.readOnly.tooltip": "无法更新告警", @@ -38949,14 +38948,10 @@ "xpack.securitySolution.event.summary.threat_indicator.showMatches": "显示所有 {count} 个指标匹配告警", "xpack.securitySolution.eventDetails.alertReason": "告警原因", "xpack.securitySolution.eventDetails.description": "描述", - "xpack.securitySolution.eventDetails.multiFieldBadge": "多字段", - "xpack.securitySolution.eventDetails.multiFieldTooltipContent": "多字段的每个字段可以有多个值", - "xpack.securitySolution.eventDetails.osqueryView": "Osquery 结果", "xpack.securitySolution.eventDetails.responseActions.endpoint.executed": "已执行 {command} 命令", "xpack.securitySolution.eventDetails.responseActions.endpoint.failed": "无法执行 {command} 命令", "xpack.securitySolution.eventDetails.responseActions.endpoint.pending": "正在执行 {command} 命令", "xpack.securitySolution.eventDetails.responseActions.endpoint.tried": "已尝试执行 {command} 命令", - "xpack.securitySolution.eventDetails.responseActionsView": "响应结果", "xpack.securitySolution.eventDetails.summaryView": "摘要", "xpack.securitySolution.eventDetails.table": "表", "xpack.securitySolution.eventDetails.table.actions": "操作", From 4eeb35d21b9d983582c25be8936591acd8a29246 Mon Sep 17 00:00:00 2001 From: Ryan Keairns Date: Mon, 19 Aug 2024 08:28:29 -0700 Subject: [PATCH 18/25] Slim down popover panels (#190472) ## Summary These couple of popover panels contain more padding than desired or intended by the design system. **Before** _Discover alerts popover_ _Nav deployments popover_ **After** _Discover alerts popover_ _Nav deployments popover_ ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [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 - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../src/project_navigation/breadcrumbs.tsx | 6 ++++-- .../main/components/top_nav/open_alerts_popover.tsx | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/breadcrumbs.tsx b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/breadcrumbs.tsx index fb1043d23952..ac80702ab99e 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/breadcrumbs.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/breadcrumbs.tsx @@ -144,6 +144,7 @@ function buildRootCrumb({ color="text" iconType="gear" data-test-subj="manageDeploymentBtn" + size="s" > {i18n.translate('core.ui.primaryNav.cloud.breadCrumbDropdown.manageDeploymentLabel', { defaultMessage: 'Manage this deployment', @@ -157,6 +158,7 @@ function buildRootCrumb({ color="text" iconType="spaces" data-test-subj="viewDeploymentsBtn" + size="s" > {cloudLinks.deployments.title} @@ -164,9 +166,9 @@ function buildRootCrumb({ ), popoverProps: { - panelPaddingSize: 'm', + panelPaddingSize: 's', zIndex: 6000, - panelStyle: { width: 260 }, + panelStyle: { maxWidth: 240 }, panelProps: { 'data-test-subj': 'deploymentLinksPanel', }, diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index 097278ac9cd6..7b97db9b6331 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -183,8 +183,9 @@ export function AlertsPopover({ button={anchorElement} closePopover={onClose} isOpen={!alertFlyoutVisible} + panelPaddingSize="s" > - + ); From 439c7fa84c45b3c632193ce0ffd16b437ea21e08 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Mon, 19 Aug 2024 11:35:25 -0400 Subject: [PATCH 19/25] [Fleet] Replace all references to unsafe YML load/dump methods in Fleet codebase (#190659) ## Summary Replaces any unsafe YML operations with their safe alternatives. `load` -> `safeLoad` `dump` -> `safeDump` --- .../custom_integrations/assets/dataset/ingest_pipeline.ts | 4 ++-- .../packages/custom_integrations/assets/dataset/manifest.ts | 4 ++-- .../epm/packages/custom_integrations/assets/manifest.ts | 4 ++-- .../plugins/fleet/server/services/epm/packages/utils.test.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/ingest_pipeline.ts b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/ingest_pipeline.ts index 0021f395158e..4d7313cacff7 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/ingest_pipeline.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/ingest_pipeline.ts @@ -5,7 +5,7 @@ * 2.0. */ -import * as yaml from 'js-yaml'; +import { safeDump } from 'js-yaml'; // NOTE: The install methods will take care of adding a reference to a @custom pipeline. We don't need to add one here. export const createDefaultPipeline = (dataset: string, type: string) => { @@ -25,5 +25,5 @@ export const createDefaultPipeline = (dataset: string, type: string) => { managed: true, }, }; - return yaml.dump(pipeline); + return safeDump(pipeline); }; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/manifest.ts b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/manifest.ts index 75b34867f6d0..efca290e3109 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/manifest.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/dataset/manifest.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import * as yaml from 'js-yaml'; +import { safeDump } from 'js-yaml'; import { convertStringToTitle } from '../../utils'; import type { AssetOptions } from '../generate'; @@ -17,5 +17,5 @@ export const createDatasetManifest = (dataset: string, assetOptions: AssetOption title: convertStringToTitle(dataset), type, }; - return yaml.dump(manifest); + return safeDump(manifest); }; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/manifest.ts b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/manifest.ts index cf308f03db7d..4c27ad6c4534 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/manifest.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/custom_integrations/assets/manifest.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import * as yaml from 'js-yaml'; +import { safeDump } from 'js-yaml'; import type { AssetOptions } from './generate'; @@ -34,5 +34,5 @@ export const createManifest = (assetOptions: AssetOptions) => { }, }; - return yaml.dump(manifest); + return safeDump(manifest); }; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/utils.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/utils.test.ts index 166687a836fb..8de21942d755 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/utils.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/utils.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { dump } from 'js-yaml'; +import { safeDump } from 'js-yaml'; import type { AssetsMap } from '../../../../common/types'; @@ -14,7 +14,7 @@ import type { RegistryDataStream } from '../../../../common'; import { resolveDataStreamFields } from './utils'; describe('resolveDataStreamFields', () => { - const statusAssetYml = dump([ + const statusAssetYml = safeDump([ { name: 'apache.status', type: 'group', From 6d1426acd8823a48a0ffa03ad6635cf853f936a2 Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Mon, 19 Aug 2024 17:59:43 +0200 Subject: [PATCH 20/25] [Security Solution][Entity details] - move code to get url link to flyout folder (#190111) --- .../right/components/header_actions.test.tsx | 10 ++--- .../right/components/header_actions.tsx | 8 ++-- .../right/hooks/use_flyout_is_expandable.ts | 2 +- .../right/hooks/use_get_flyout_link.test.tsx | 37 ++++++++++++++++++ .../right/hooks/use_get_flyout_link.ts} | 39 +++++++++++++------ .../right/hooks/use_process_data.ts | 2 +- .../right/hooks/use_session_preview.test.tsx | 2 +- .../right/hooks/use_session_preview.ts | 2 +- .../document_details/shared/context.tsx | 2 +- .../shared/hooks/use_event_details.test.tsx | 28 +++++++++++-- .../shared/hooks/use_event_details.ts | 23 +++++++++-- .../shared/hooks/use_get_fields_data.test.tsx | 36 +++++++++++++++++ .../shared}/hooks/use_get_fields_data.ts | 37 ++++++++++++++---- .../use_show_related_alerts_by_ancestry.ts | 2 +- ...how_related_alerts_by_same_source_event.ts | 2 +- .../use_show_related_alerts_by_session.ts | 2 +- .../shared/hooks/use_show_related_cases.ts | 2 +- .../hooks/use_show_suppressed_alerts.ts | 2 +- .../shared/mocks/mock_get_fields_data.ts | 2 +- 19 files changed, 192 insertions(+), 48 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.test.tsx rename x-pack/plugins/security_solution/public/{timelines/components/side_panel/event_details/use_get_alert_details_flyout_link.ts => flyout/document_details/right/hooks/use_get_flyout_link.ts} (64%) create mode 100644 x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.test.tsx rename x-pack/plugins/security_solution/public/{common => flyout/document_details/shared}/hooks/use_get_fields_data.ts (86%) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.test.tsx index 26d2c9d1f63d..a3aa8e410eee 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.test.tsx @@ -14,13 +14,11 @@ import { useAssistant } from '../hooks/use_assistant'; import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; import { TestProvidersComponent } from '../../../../common/mock'; -import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link'; +import { useGetFlyoutLink } from '../hooks/use_get_flyout_link'; jest.mock('../../../../common/lib/kibana'); jest.mock('../hooks/use_assistant'); -jest.mock( - '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link' -); +jest.mock('../hooks/use_get_flyout_link'); jest.mock('@elastic/eui', () => ({ ...jest.requireActual('@elastic/eui'), @@ -53,7 +51,7 @@ describe('', () => { beforeEach(() => { window.location.search = '?'; - jest.mocked(useGetAlertDetailsFlyoutLink).mockReturnValue(alertUrl); + jest.mocked(useGetFlyoutLink).mockReturnValue(alertUrl); jest.mocked(useAssistant).mockReturnValue({ showAssistant: true, promptContextId: '' }); }); @@ -65,7 +63,7 @@ describe('', () => { }); it('should not render share button in the title if alert is missing url info', () => { - jest.mocked(useGetAlertDetailsFlyoutLink).mockReturnValue(null); + jest.mocked(useGetFlyoutLink).mockReturnValue(null); const { queryByTestId } = renderHeaderActions(mockContextValue); expect(queryByTestId(SHARE_BUTTON_TEST_ID)).not.toBeInTheDocument(); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx index f90d67f87f37..078f273ec28f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx @@ -10,7 +10,7 @@ import React, { memo } from 'react'; import { EuiButtonIcon, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NewChatByTitle } from '@kbn/elastic-assistant'; -import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link'; +import { useGetFlyoutLink } from '../hooks/use_get_flyout_link'; import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { useAssistant } from '../hooks/use_assistant'; import { @@ -27,9 +27,9 @@ export const HeaderActions: VFC = memo(() => { const { dataFormattedForFieldBrowser, eventId, indexName } = useDocumentDetailsContext(); const { isAlert, timestamp } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); - const alertDetailsLink = useGetAlertDetailsFlyoutLink({ - _id: eventId, - _index: indexName, + const alertDetailsLink = useGetFlyoutLink({ + eventId, + indexName, timestamp, }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.ts index f1f762fa9abd..8098acef40d2 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_flyout_is_expandable.ts @@ -8,7 +8,7 @@ import { useMemo } from 'react'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { getField, getFieldArray } from '../../shared/utils'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; import { getRowRenderer } from '../../../../timelines/components/timeline/body/renderers/get_row_renderer'; import { defaultRowRenderers } from '../../../../timelines/components/timeline/body/renderers'; import { isEcsAllowedValue } from '../utils/event_utils'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.test.tsx new file mode 100644 index 000000000000..2db21334e59f --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useGetFlyoutLink } from './use_get_flyout_link'; +import { useGetAppUrl } from '@kbn/security-solution-navigation'; +import { ALERT_DETAILS_REDIRECT_PATH } from '../../../../../common/constants'; + +jest.mock('@kbn/security-solution-navigation'); + +const eventId = 'eventId'; +const indexName = 'indexName'; +const timestamp = 'timestamp'; + +describe('useGetFlyoutLink', () => { + it('should return url', () => { + (useGetAppUrl as jest.Mock).mockReturnValue({ + getAppUrl: (data: { path: string }) => data.path, + }); + + const hookResult = renderHook(() => + useGetFlyoutLink({ + eventId, + indexName, + timestamp, + }) + ); + + const origin = 'http://localhost'; + const path = `${ALERT_DETAILS_REDIRECT_PATH}/${eventId}?index=${indexName}×tamp=${timestamp}`; + expect(hookResult.result.current).toBe(`${origin}${path}`); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/use_get_alert_details_flyout_link.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.ts similarity index 64% rename from x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/use_get_alert_details_flyout_link.ts rename to x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.ts index 5838099820dd..b4f50f1e89a3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/use_get_alert_details_flyout_link.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_get_flyout_link.ts @@ -11,19 +11,36 @@ import { DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants'; import { buildAlertDetailPath } from '../../../../../common/utils/alert_detail_path'; import { useAppUrl } from '../../../../common/lib/kibana/hooks'; -// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 -export const useGetAlertDetailsFlyoutLink = ({ - _id, - _index, - timestamp, -}: { - _id: string; - _index: string; +export interface UseGetFlyoutLinkProps { + /** + * Id of the document + */ + eventId: string; + /** + * Name of the index used in the parent's page + */ + indexName: string; + /** + * Timestamp of the document + */ timestamp: string; -}) => { +} + +/** + * Hook to get the link to the alert details page + */ +export const useGetFlyoutLink = ({ + eventId, + indexName, + timestamp, +}: UseGetFlyoutLinkProps): string | null => { const { getAppUrl } = useAppUrl(); - const alertDetailPath = buildAlertDetailPath({ alertId: _id, index: _index, timestamp }); - const isPreviewAlert = _index.includes(DEFAULT_PREVIEW_INDEX); + const alertDetailPath = buildAlertDetailPath({ + alertId: eventId, + index: indexName, + timestamp, + }); + const isPreviewAlert = indexName.includes(DEFAULT_PREVIEW_INDEX); // getAppUrl accounts for the users selected space const alertDetailsLink = useMemo(() => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_process_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_process_data.ts index 8f02f371a531..bb4e2be802a1 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_process_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_process_data.ts @@ -7,7 +7,7 @@ import { useMemo } from 'react'; import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; import { getField } from '../../shared/utils'; import { useDocumentDetailsContext } from '../../shared/context'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx index 64e10766ad21..0f6f23377262 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx @@ -10,7 +10,7 @@ import { renderHook } from '@testing-library/react-hooks'; import type { UseSessionPreviewParams } from './use_session_preview'; import { useSessionPreview } from './use_session_preview'; import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser'; import { mockFieldData, mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts index 95c79e6815bf..4b2132d26587 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts @@ -7,7 +7,7 @@ import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from '../../shared/hooks/use_get_fields_data'; import { getField } from '../../shared/utils'; import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx index bdfae953303c..12e2ad4f2a0b 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx @@ -15,7 +15,7 @@ import { FlyoutLoading } from '../../shared/components/flyout_loading'; import type { SearchHit } from '../../../../common/search_strategy'; import { useBasicDataFromDetailsData } from './hooks/use_basic_data_from_details_data'; import type { DocumentDetailsProps } from './types'; -import type { GetFieldsData } from '../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from './hooks/use_get_fields_data'; import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; export interface DocumentDetailsContext { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.test.tsx index 7eb2c76573a2..de1020bac4d0 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.test.tsx @@ -8,22 +8,42 @@ import type { RenderHookResult } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react-hooks'; import type { UseEventDetailsParams, UseEventDetailsResult } from './use_event_details'; -import { useEventDetails } from './use_event_details'; +import { getAlertIndexAlias, useEventDetails } from './use_event_details'; import { useSpaceId } from '../../../../common/hooks/use_space_id'; import { useRouteSpy } from '../../../../common/utils/route/use_route_spy'; import { useSourcererDataView } from '../../../../sourcerer/containers'; import { useTimelineEventsDetails } from '../../../../timelines/containers/details'; -import { useGetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import { useGetFieldsData } from './use_get_fields_data'; jest.mock('../../../../common/hooks/use_space_id'); jest.mock('../../../../common/utils/route/use_route_spy'); jest.mock('../../../../sourcerer/containers'); jest.mock('../../../../timelines/containers/details'); -jest.mock('../../../../common/hooks/use_get_fields_data'); +jest.mock('./use_get_fields_data'); const eventId = 'eventId'; const indexName = 'indexName'; +describe('getAlertIndexAlias', () => { + it('should handle default alert index', () => { + expect(getAlertIndexAlias('.internal.alerts-security.alerts')).toEqual( + '.alerts-security.alerts-default' + ); + }); + + it('should handle default preview index', () => { + expect(getAlertIndexAlias('.internal.preview.alerts-security.alerts')).toEqual( + '.preview.alerts-security.alerts-default' + ); + }); + + it('should handle non default space id', () => { + expect(getAlertIndexAlias('.internal.preview.alerts-security.alerts', 'test')).toEqual( + '.preview.alerts-security.alerts-test' + ); + }); +}); + describe('useEventDetails', () => { let hookResult: RenderHookResult; @@ -35,7 +55,7 @@ describe('useEventDetails', () => { indexPattern: {}, }); (useTimelineEventsDetails as jest.Mock).mockReturnValue([false, [], {}, {}, jest.fn()]); - jest.mocked(useGetFieldsData).mockReturnValue((field: string) => field); + jest.mocked(useGetFieldsData).mockReturnValue({ getFieldsData: (field: string) => field }); hookResult = renderHook(() => useEventDetails({ eventId, indexName })); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.ts index b039cc9573f3..40acb8690ce6 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.ts @@ -9,16 +9,31 @@ import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-pl import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { SecurityPageName } from '@kbn/security-solution-navigation'; import type { DataViewBase } from '@kbn/es-query'; +import { DEFAULT_ALERTS_INDEX, DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants'; import type { RunTimeMappings } from '../../../../../common/api/search_strategy'; import { useSpaceId } from '../../../../common/hooks/use_space_id'; -import { getAlertIndexAlias } from '../../../../timelines/components/side_panel/event_details/helpers'; import { useRouteSpy } from '../../../../common/utils/route/use_route_spy'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { useSourcererDataView } from '../../../../sourcerer/containers'; import { useTimelineEventsDetails } from '../../../../timelines/containers/details'; -import { useGetFieldsData } from '../../../../common/hooks/use_get_fields_data'; import type { SearchHit } from '../../../../../common/search_strategy'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from './use_get_fields_data'; +import { useGetFieldsData } from './use_get_fields_data'; + +/** + * The referenced alert _index in the flyout uses the `.internal.` such as `.internal.alerts-security.alerts-spaceId` in the alert page flyout and .internal.preview.alerts-security.alerts-spaceId` in the rule creation preview flyout, + * but we always want to use their respective aliase indices rather than accessing their backing .internal. indices. + */ +export const getAlertIndexAlias = ( + index: string, + spaceId: string = 'default' +): string | undefined => { + if (index.startsWith(`.internal${DEFAULT_ALERTS_INDEX}`)) { + return `${DEFAULT_ALERTS_INDEX}-${spaceId}`; + } else if (index.startsWith(`.internal${DEFAULT_PREVIEW_INDEX}`)) { + return `${DEFAULT_PREVIEW_INDEX}-${spaceId}`; + } +}; export interface UseEventDetailsParams { /** @@ -90,7 +105,7 @@ export const useEventDetails = ({ runtimeMappings: sourcererDataView?.sourcererDataView?.runtimeFieldMap as RunTimeMappings, skip: !eventId, }); - const getFieldsData = useGetFieldsData(searchHit?.fields); + const { getFieldsData } = useGetFieldsData({ fieldsData: searchHit?.fields }); return { browserFields: sourcererDataView.browserFields, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.test.tsx new file mode 100644 index 000000000000..fcf370b4bca1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import { mockSearchHit } from '../mocks/mock_search_hit'; +import type { UseGetFieldsDataParams, UseGetFieldsDataResult } from './use_get_fields_data'; +import { useGetFieldsData } from './use_get_fields_data'; + +const fieldsData = { + ...mockSearchHit.fields, + field: ['value'], +}; + +describe('useGetFieldsData', () => { + let hookResult: RenderHookResult; + + it('should return the value for a field', () => { + hookResult = renderHook(() => useGetFieldsData({ fieldsData })); + + const getFieldsData = hookResult.result.current.getFieldsData; + expect(getFieldsData('field')).toEqual(['value']); + expect(getFieldsData('wrong_field')).toEqual(undefined); + }); + + it('should handle undefined', () => { + hookResult = renderHook(() => useGetFieldsData({ fieldsData: undefined })); + + const getFieldsData = hookResult.result.current.getFieldsData; + expect(getFieldsData('field')).toEqual(undefined); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.ts similarity index 86% rename from x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts rename to x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.ts index 12f6c5fbd0cb..3e055e3bc4f6 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_get_fields_data.ts @@ -7,7 +7,7 @@ import { useCallback, useMemo } from 'react'; import { getOr } from 'lodash/fp'; -import type { SearchHit } from '../../../common/search_strategy'; +import type { SearchHit } from '../../../../../common/search_strategy'; /** * Since the fields api may return a string array as well as an object array @@ -37,7 +37,6 @@ const getAllDotIndicesInReverse = (dotField: string): number[] => { /** * We get the dot paths so we can look up each path to see if any of the nested fields exist * */ - const getAllPotentialDotPaths = (dotField: string): string[][] => { const reverseDotIndices = getAllDotIndicesInReverse(dotField); @@ -49,6 +48,9 @@ const getAllPotentialDotPaths = (dotField: string): string[][] => { return pathTuples; }; +/** + * We get the nested value + */ const getNestedValue = (startPath: string, endPath: string, data: Record) => { const foundPrimaryPath = data[startPath]; if (Array.isArray(foundPrimaryPath)) { @@ -63,7 +65,7 @@ const getNestedValue = (startPath: string, endPath: string, data: Record GetFieldsDataValue; +export type GetFieldsData = (field: string) => string | string[] | null | undefined; + +export interface UseGetFieldsDataParams { + /** + * All fields from the searchHit result + */ + fieldsData: SearchHit['fields'] | undefined; +} + +export interface UseGetFieldsDataResult { + /** + * Retrieves the value for the provided field (reading from the searchHit result) + */ + getFieldsData: GetFieldsData; +} -// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 -export const useGetFieldsData = (fieldsData: SearchHit['fields'] | undefined): GetFieldsData => { +/** + * Hook that returns a function to retrieve the values for a field (reading from the searchHit result) + */ +export const useGetFieldsData = ({ + fieldsData, +}: UseGetFieldsDataParams): UseGetFieldsDataResult => { // TODO: Move cache to top level container such as redux or context. Make it store type agnostic if possible // TODO: Handle updates where data is re-requested and the cache is reset. const cachedOriginalData = useMemo(() => fieldsData, [fieldsData]); @@ -111,7 +130,7 @@ export const useGetFieldsData = (fieldsData: SearchHit['fields'] | undefined): G [cachedExpensiveNestedValues] ); - return useCallback( + const getFieldsData = useCallback( (field: string) => { let fieldsValue; // Get an expensive value from the cache if it exists, otherwise search for the value @@ -133,4 +152,6 @@ export const useGetFieldsData = (fieldsData: SearchHit['fields'] | undefined): G }, [cacheNestedValues, cachedExpensiveNestedValues, cachedOriginalData] ); + + return { getFieldsData }; }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_ancestry.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_ancestry.ts index 12172621b4df..69c0ae298935 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_ancestry.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_ancestry.ts @@ -6,7 +6,7 @@ */ import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from './use_get_fields_data'; import { useIsInvestigateInResolverActionEnabled } from '../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; import { useLicense } from '../../../../common/hooks/use_license'; import { ANCESTOR_ID } from '../constants/field_names'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_same_source_event.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_same_source_event.ts index 2f76c74b329d..cbee250120a0 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_same_source_event.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_same_source_event.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from './use_get_fields_data'; import { ANCESTOR_ID } from '../constants/field_names'; import { getField } from '../utils'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_session.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_session.ts index 81ce4bdb0475..de7af4975da9 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_session.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_alerts_by_session.ts @@ -6,7 +6,7 @@ */ import { ENTRY_LEADER_ENTITY_ID } from '../constants/field_names'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from './use_get_fields_data'; import { getField } from '../utils'; export interface UseShowRelatedAlertsBySessionParams { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts index 4a739dd930e1..ba40f74417c5 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts @@ -7,7 +7,7 @@ import { APP_ID } from '../../../../../common'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from './use_get_fields_data'; import { getField } from '../utils'; export interface UseShowRelatedCasesParams { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_suppressed_alerts.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_suppressed_alerts.ts index f459d83e5f3d..df0abc1809f2 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_suppressed_alerts.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_suppressed_alerts.ts @@ -6,7 +6,7 @@ */ import { ALERT_SUPPRESSION_DOCS_COUNT } from '@kbn/rule-data-utils'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from './use_get_fields_data'; export interface ShowSuppressedAlertsParams { /** diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_get_fields_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_get_fields_data.ts index 4db7cf262580..bf8b8cbeae42 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_get_fields_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_get_fields_data.ts @@ -12,7 +12,7 @@ import { ALERT_SUPPRESSION_DOCS_COUNT, } from '@kbn/rule-data-utils'; import { EventKind } from '../constants/event_kinds'; -import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; +import type { GetFieldsData } from '../hooks/use_get_fields_data'; export const mockFieldData: Record = { [ALERT_SEVERITY]: ['low'], From b82c49f825a2ba7cd8c28d33fc18fa18fefc39bb Mon Sep 17 00:00:00 2001 From: Kevin Lacabane Date: Mon, 19 Aug 2024 18:01:22 +0200 Subject: [PATCH 21/25] [kbn-data-forge] fix mongodb duplicate component name (#190660) Noticed this error during data forge resources cleanup caused by duplicated name ``` info Deleteing components for logs-mongodb@template (mongodb_8.0.0_base,mongodb_8.0.0_log,mongodb_8.0.0_host,mongodb_8.0.0_host) ERROR Failed to delete {"options":{"redaction":{"type":"replace","additionalKeys":[]}},"name":"ResponseError","meta":{"body":{"error":{"root_cause":[{"type":"resource_not_found_exception","reason":"mongodb_8.0.0_host"}],"type":"resource_not_found_exception","reason":"mongodb_8.0.0_host"}, ``` --- .../src/data_sources/fake_stack/mongodb/ecs/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/mongodb/ecs/index.ts b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/mongodb/ecs/index.ts index 758c9f5b8e2c..f1a994a37a24 100644 --- a/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/mongodb/ecs/index.ts +++ b/x-pack/packages/kbn-data-forge/src/data_sources/fake_stack/mongodb/ecs/index.ts @@ -21,7 +21,7 @@ const components = [ { name: `${MONGODB}_${ECS_VERSION}_base`, template: base }, { name: `${MONGODB}_${ECS_VERSION}_log`, template: log }, { name: `${MONGODB}_${ECS_VERSION}_host`, template: host }, - { name: `${MONGODB}_${ECS_VERSION}_host`, template: mongodb }, + { name: `${MONGODB}_${ECS_VERSION}_mongodb`, template: mongodb }, ]; export const indexTemplate: IndexTemplateDef = { From f64392f32fc74efed2ab6dac8e05c6655f72073d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 19 Aug 2024 17:12:09 +0100 Subject: [PATCH 22/25] skip flaky suite (#189791) --- .../timeline/tabs/query/query_tab_unified_components.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx index 1644982533a9..81be06e52e44 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx @@ -292,7 +292,8 @@ describe('query tab with unified timeline', () => { ); }); - describe('pagination', () => { + // FLAKY: https://github.com/elastic/kibana/issues/189791 + describe.skip('pagination', () => { beforeEach(() => { // should return all the records instead just 3 // as the case in the default mock From a9e8d4a37a44c3ae9c3a70e90e67f399f68e5ec6 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 19 Aug 2024 17:13:20 +0100 Subject: [PATCH 23/25] skip flaky suite (#189792) --- .../timeline/tabs/query/query_tab_unified_components.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx index 81be06e52e44..4ee2c1565acd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx @@ -596,7 +596,8 @@ describe('query tab with unified timeline', () => { ); }); - describe('left controls', () => { + // FLAKY: https://github.com/elastic/kibana/issues/189792 + describe.skip('left controls', () => { it( 'should clear all sorting', async () => { From 6920cc13de846b48ec3cd3cb5775188c053fca29 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 19 Aug 2024 17:13:45 +0100 Subject: [PATCH 24/25] skip flaky suite (#189793) --- .../timeline/tabs/query/query_tab_unified_components.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx index 4ee2c1565acd..db808346fa4e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx @@ -597,6 +597,7 @@ describe('query tab with unified timeline', () => { }); // FLAKY: https://github.com/elastic/kibana/issues/189792 + // FLAKY: https://github.com/elastic/kibana/issues/189793 describe.skip('left controls', () => { it( 'should clear all sorting', From 754595b12396f20c0a547b15c889b355f97e7799 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 19 Aug 2024 17:14:20 +0100 Subject: [PATCH 25/25] skip flaky suite (#189794) --- .../timeline/tabs/query/query_tab_unified_components.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx index db808346fa4e..8b7b3742f0f2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx @@ -835,7 +835,8 @@ describe('query tab with unified timeline', () => { }); describe('Leading actions - notes', () => { - describe('securitySolutionNotesEnabled = true', () => { + // FLAKY: https://github.com/elastic/kibana/issues/189794 + describe.skip('securitySolutionNotesEnabled = true', () => { beforeEach(() => { (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( jest.fn((feature: keyof ExperimentalFeatures) => {