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/package.json b/package.json index ea58596f6ba4..46acb67cba18 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/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/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/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/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); } } 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" > - + ); 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/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/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/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); }); 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/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 = { 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/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/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/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/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/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/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/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/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/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/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', 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/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/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; 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/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."` ); }); 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/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/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 91% 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 c1a57c6a9ab7..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,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 '../../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/flyout/document_details/left/components/investigation_guide_view.tsx similarity index 76% 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 29e2354f7454..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,23 +8,30 @@ 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 * 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; 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 */ @@ -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/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..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 @@ -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,14 +42,13 @@ describe('useThreatIntelligenceDetails', () => { jest .mocked(useBasicDataFromDetailsData) - .mockReturnValue({ isAlert: true } as unknown as GetBasicDataFromDetailsData); + .mockReturnValue({ isAlert: true } as unknown as UseBasicDataFromDetailsDataResult); jest.mocked(useSourcererDataView).mockReturnValue({ browserFields: {}, dataViewId: '', loading: false, indicesExist: true, - patternList: [], selectedPatterns: [], indexPattern: { fields: [], title: '' }, sourcererDataView: undefined, 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/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/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.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 00fbd9303c33..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,8 +10,8 @@ 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 { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +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 { ALERT_SUMMARY_CONVERSATION_ID, @@ -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/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/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/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/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/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_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 28985471a394..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'; @@ -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..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,9 +7,9 @@ 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 '../../../../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/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/context.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx index 1197e39ad86c..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 @@ -13,9 +13,9 @@ 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 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_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_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_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_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, }); 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_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/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_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/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'], 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', 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. 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/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..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 @@ -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 @@ -595,7 +596,9 @@ describe('query tab with unified timeline', () => { ); }); - describe('left controls', () => { + // 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', async () => { @@ -832,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) => { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b134c6acfc58..cc0f7067d89e 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", @@ -7924,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", @@ -19622,10 +19626,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", @@ -35557,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", @@ -38922,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 39b898482ad0..519c9f2a428a 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": "フィールドを表から削除", @@ -7918,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": "更新", @@ -19610,10 +19614,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": "エージェントポリシーを作成", @@ -35541,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": "アラートを更新できません", @@ -38903,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 0d6c56ae59a3..328008ebf395 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": "从表中移除字段", @@ -7931,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": "刷新", @@ -19636,10 +19640,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": "创建代理策略", @@ -35582,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": "无法更新告警", @@ -38947,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": "操作", 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 } } }, + }, + }, + }); + }); +} 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/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); }); }); } 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'); + }, }; } 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 () => { 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', 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, + }); + } +); diff --git a/yarn.lock b/yarn.lock index 5c8dcdde1999..8f0bb0d6fe20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16590,10 +16590,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" @@ -16613,7 +16613,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" @@ -20004,10 +20004,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"