diff --git a/config/serverless.yml b/config/serverless.yml index 8167ee5a42f4..d05efe44e12e 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -47,6 +47,11 @@ xpack.cloud_integrations.data_migration.enabled: false data.search.sessions.enabled: false advanced_settings.enabled: false +# Disable UI of security management plugins +xpack.security.ui.userManagementEnabled: false +xpack.security.ui.roleManagementEnabled: false +xpack.security.ui.roleMappingManagementEnabled: false + # Enforce restring access to internal APIs see https://github.com/elastic/kibana/issues/151940 # server.restrictInternalApis: true # Telemetry enabled by default and not disableable via UI diff --git a/docs/user/alerting/create-and-manage-rules.asciidoc b/docs/user/alerting/create-and-manage-rules.asciidoc index 49dd5d2019b8..ed21a2bc8b22 100644 --- a/docs/user/alerting/create-and-manage-rules.asciidoc +++ b/docs/user/alerting/create-and-manage-rules.asciidoc @@ -169,9 +169,8 @@ image::images/rules-imported-banner.png[Rules import banner,500] [[rule-details]] === View rule details -You can determine the health of a rule by looking at the *Last response* column -in *{stack-manage-app}* > *{rules-ui}*. A rule can have one of the following -responses: +You can determine the health of a rule by looking at the *Last response* in *{stack-manage-app}* > *{rules-ui}*. +A rule can have one of the following responses: `failed`:: The rule ran with errors. `succeeded`:: The rule ran without errors. @@ -193,6 +192,14 @@ When an alert is created, it generates actions. If the conditions that caused th NOTE: The `flapping` state is possible only if you have enabled alert flapping detection in *{stack-manage-app}* > *{rules-ui}* > *Settings*. For each space, you can choose a look back window and threshold that are used to determine whether alerts are flapping. For example, you can specify that the alert must change status at least 6 times in the last 10 runs. If the rule has actions that run when the alert status changes, those actions are suppressed while the alert is flapping. +If there are rule actions that failed to run successfully, you can see the details on the *History* tab. +In the *Message* column, click the warning or expand icon image:images/expand-icon-2.png[double arrow icon to open a flyout with the document details] or click the number in the *Errored actions* column to open the *Errored Actions* panel. +In this example, the action failed because the <> setting was updated and the action's email recipient is no longer included in the allowlist: + +[role="screenshot"] +image::images/rule-details-errored-actions.png[Rule histor page with alerts that have errored actions] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. + If an alert was affected by a maintenance window, its identifier appears in the *Maintenance windows* column. For more information about their impact on alert notifications, refer to <>. diff --git a/docs/user/alerting/images/rule-details-errored-actions.png b/docs/user/alerting/images/rule-details-errored-actions.png new file mode 100644 index 000000000000..927ce0db6a39 Binary files /dev/null and b/docs/user/alerting/images/rule-details-errored-actions.png differ diff --git a/package.json b/package.json index 9c4e285ce8ab..e5295bc888c3 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "@dnd-kit/utilities": "^2.0.0", "@elastic/apm-rum": "^5.12.0", "@elastic/apm-rum-react": "^1.4.2", - "@elastic/charts": "59.0.0", + "@elastic/charts": "59.1.0", "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.8.0-canary.2", "@elastic/ems-client": "8.4.0", diff --git a/packages/kbn-dom-drag-drop/src/drag_drop.tsx b/packages/kbn-dom-drag-drop/src/drag_drop.tsx index 66cb11bc15ad..ab4158ad3154 100644 --- a/packages/kbn-dom-drag-drop/src/drag_drop.tsx +++ b/packages/kbn-dom-drag-drop/src/drag_drop.tsx @@ -628,21 +628,6 @@ const DropsInner = memo(function DropsInner(props: DropsInnerProps) { const mainTargetProps = getProps(dropTypes && dropTypes[0]); - const extraDropStyles = useMemo(() => { - const extraDrops = dropTypes && dropTypes.length && dropTypes.slice(1); - if (!extraDrops || !extraDrops.length) { - return; - } - - const height = extraDrops.length * 40; - const minHeight = height - (mainTargetRef.current?.clientHeight || 40); - const clipPath = `polygon(100% 0px, 100% ${height - minHeight}px, 0 100%, 0 0)`; - return { - clipPath, - height, - }; - }, [dropTypes]); - return (
{dropTypes && dropTypes.length > 1 && ( - <> -
- - {dropTypes.slice(1).map((dropType) => { - const dropChildren = getCustomDropTarget?.(dropType); - return dropChildren ? ( - - - {dropChildren} - - - ) : null; - })} - - + + {dropTypes.slice(1).map((dropType) => { + const dropChildren = getCustomDropTarget?.(dropType); + return dropChildren ? ( + + + {dropChildren} + + + ) : null; + })} + )}
); diff --git a/packages/kbn-dom-drag-drop/src/sass/drag_drop.scss b/packages/kbn-dom-drag-drop/src/sass/drag_drop.scss index ffea63cf94ce..d586d86761cb 100644 --- a/packages/kbn-dom-drag-drop/src/sass/drag_drop.scss +++ b/packages/kbn-dom-drag-drop/src/sass/drag_drop.scss @@ -154,19 +154,12 @@ $reorderItemMargin: $euiSizeS; visibility: visible; } -.domDragDrop__diamondPath { - position: absolute; - width: 30%; - top: 0; - left: -$euiSize; - z-index: $domDragDropZLevel0; -} - .domDragDrop__extraDropWrapper { position: relative; width: 100%; height: 100%; background: $euiColorLightestShade; + border-radius: $euiSizeXS; .domDragDrop__extraDrop, .domDragDrop__extraDrop:before { diff --git a/packages/kbn-expandable-flyout/index.ts b/packages/kbn-expandable-flyout/index.ts index cc423eb27509..576525206d4e 100644 --- a/packages/kbn-expandable-flyout/index.ts +++ b/packages/kbn-expandable-flyout/index.ts @@ -16,4 +16,4 @@ export { export type { ExpandableFlyoutApi } from './src/context'; export type { ExpandableFlyoutProps } from './src'; -export type { FlyoutPanel } from './src/types'; +export type { FlyoutPanelProps } from './src/types'; diff --git a/packages/kbn-expandable-flyout/src/actions.ts b/packages/kbn-expandable-flyout/src/actions.ts index 570921439430..aa8e813f8a84 100644 --- a/packages/kbn-expandable-flyout/src/actions.ts +++ b/packages/kbn-expandable-flyout/src/actions.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FlyoutPanel } from './types'; +import { FlyoutPanelProps } from './types'; export enum ActionType { openFlyout = 'open_flyout', @@ -24,22 +24,22 @@ export type Action = | { type: ActionType.openFlyout; payload: { - right?: FlyoutPanel; - left?: FlyoutPanel; - preview?: FlyoutPanel; + right?: FlyoutPanelProps; + left?: FlyoutPanelProps; + preview?: FlyoutPanelProps; }; } | { type: ActionType.openRightPanel; - payload: FlyoutPanel; + payload: FlyoutPanelProps; } | { type: ActionType.openLeftPanel; - payload: FlyoutPanel; + payload: FlyoutPanelProps; } | { type: ActionType.openPreviewPanel; - payload: FlyoutPanel; + payload: FlyoutPanelProps; } | { type: ActionType.closeRightPanel; diff --git a/packages/kbn-expandable-flyout/src/context.tsx b/packages/kbn-expandable-flyout/src/context.tsx index b7ad721a2b9f..9738c2a4c867 100644 --- a/packages/kbn-expandable-flyout/src/context.tsx +++ b/packages/kbn-expandable-flyout/src/context.tsx @@ -17,7 +17,7 @@ import React, { } from 'react'; import { ActionType } from './actions'; import { reducer, State } from './reducer'; -import type { FlyoutPanel } from './types'; +import type { FlyoutPanelProps } from './types'; import { initialState } from './reducer'; export interface ExpandableFlyoutContext { @@ -28,19 +28,23 @@ export interface ExpandableFlyoutContext { /** * Open the flyout with left, right and/or preview panels */ - openFlyout: (panels: { left?: FlyoutPanel; right?: FlyoutPanel; preview?: FlyoutPanel }) => void; + openFlyout: (panels: { + left?: FlyoutPanelProps; + right?: FlyoutPanelProps; + preview?: FlyoutPanelProps; + }) => void; /** * Replaces the current right panel with a new one */ - openRightPanel: (panel: FlyoutPanel) => void; + openRightPanel: (panel: FlyoutPanelProps) => void; /** * Replaces the current left panel with a new one */ - openLeftPanel: (panel: FlyoutPanel) => void; + openLeftPanel: (panel: FlyoutPanelProps) => void; /** * Add a new preview panel to the list of current preview panels */ - openPreviewPanel: (panel: FlyoutPanel) => void; + openPreviewPanel: (panel: FlyoutPanelProps) => void; /** * Closes right panel */ @@ -111,25 +115,25 @@ export const ExpandableFlyoutProvider = React.forwardRef< left, preview, }: { - right?: FlyoutPanel; - left?: FlyoutPanel; - preview?: FlyoutPanel; + right?: FlyoutPanelProps; + left?: FlyoutPanelProps; + preview?: FlyoutPanelProps; }) => dispatch({ type: ActionType.openFlyout, payload: { left, right, preview } }), [dispatch] ); const openRightPanel = useCallback( - (panel: FlyoutPanel) => dispatch({ type: ActionType.openRightPanel, payload: panel }), + (panel: FlyoutPanelProps) => dispatch({ type: ActionType.openRightPanel, payload: panel }), [] ); const openLeftPanel = useCallback( - (panel: FlyoutPanel) => dispatch({ type: ActionType.openLeftPanel, payload: panel }), + (panel: FlyoutPanelProps) => dispatch({ type: ActionType.openLeftPanel, payload: panel }), [] ); const openPreviewPanel = useCallback( - (panel: FlyoutPanel) => dispatch({ type: ActionType.openPreviewPanel, payload: panel }), + (panel: FlyoutPanelProps) => dispatch({ type: ActionType.openPreviewPanel, payload: panel }), [] ); diff --git a/packages/kbn-expandable-flyout/src/index.tsx b/packages/kbn-expandable-flyout/src/index.tsx index 1d96b72d66b3..fa9a1bf5af7c 100644 --- a/packages/kbn-expandable-flyout/src/index.tsx +++ b/packages/kbn-expandable-flyout/src/index.tsx @@ -13,7 +13,7 @@ import { EuiFlexGroup, EuiFlyout } from '@elastic/eui'; import { useExpandableFlyoutContext } from './context'; import { PreviewSection } from './components/preview_section'; import { RightSection } from './components/right_section'; -import type { FlyoutPanel, Panel } from './types'; +import type { FlyoutPanelProps, Panel } from './types'; import { LeftSection } from './components/left_section'; export interface ExpandableFlyoutProps extends Omit { @@ -98,13 +98,13 @@ export const ExpandableFlyout: React.FC = ({ > {leftSection && left ? ( ) : null} {rightSection && right ? ( ) : null} @@ -112,7 +112,7 @@ export const ExpandableFlyout: React.FC = ({ {previewSection && preview ? ( diff --git a/packages/kbn-expandable-flyout/src/reducer.test.ts b/packages/kbn-expandable-flyout/src/reducer.test.ts index 21128ded7b58..d5deeb6c0b03 100644 --- a/packages/kbn-expandable-flyout/src/reducer.test.ts +++ b/packages/kbn-expandable-flyout/src/reducer.test.ts @@ -6,32 +6,32 @@ * Side Public License, v 1. */ -import { FlyoutPanel } from './types'; +import { FlyoutPanelProps } from './types'; import { initialState, reducer, State } from './reducer'; import { Action, ActionType } from './actions'; -const rightPanel1: FlyoutPanel = { +const rightPanel1: FlyoutPanelProps = { id: 'right1', path: ['path'], }; -const leftPanel1: FlyoutPanel = { +const leftPanel1: FlyoutPanelProps = { id: 'left1', params: { id: 'id' }, }; -const previewPanel1: FlyoutPanel = { +const previewPanel1: FlyoutPanelProps = { id: 'preview1', state: { id: 'state' }, }; -const rightPanel2: FlyoutPanel = { +const rightPanel2: FlyoutPanelProps = { id: 'right2', path: ['path'], }; -const leftPanel2: FlyoutPanel = { +const leftPanel2: FlyoutPanelProps = { id: 'left2', params: { id: 'id' }, }; -const previewPanel2: FlyoutPanel = { +const previewPanel2: FlyoutPanelProps = { id: 'preview2', state: { id: 'state' }, }; diff --git a/packages/kbn-expandable-flyout/src/reducer.ts b/packages/kbn-expandable-flyout/src/reducer.ts index bb0ff125f546..5b4c0675548a 100644 --- a/packages/kbn-expandable-flyout/src/reducer.ts +++ b/packages/kbn-expandable-flyout/src/reducer.ts @@ -6,22 +6,22 @@ * Side Public License, v 1. */ -import { FlyoutPanel } from './types'; +import { FlyoutPanelProps } from './types'; import { Action, ActionType } from './actions'; export interface State { /** * Panel to render in the left section */ - left: FlyoutPanel | undefined; + left: FlyoutPanelProps | undefined; /** * Panel to render in the right section */ - right: FlyoutPanel | undefined; + right: FlyoutPanelProps | undefined; /** * Panels to render in the preview section */ - preview: FlyoutPanel[]; + preview: FlyoutPanelProps[]; } export const initialState: State = { @@ -90,7 +90,7 @@ export function reducer(state: State, action: Action) { * Navigates to the previous preview panel by removing the last entry in the array of preview panels. */ case ActionType.previousPreviewPanel: { - const p: FlyoutPanel[] = [...state.preview]; + const p: FlyoutPanelProps[] = [...state.preview]; p.pop(); return { ...state, preview: p }; } diff --git a/packages/kbn-expandable-flyout/src/types.ts b/packages/kbn-expandable-flyout/src/types.ts index b27ac4afd6fc..883f0b7aefc0 100644 --- a/packages/kbn-expandable-flyout/src/types.ts +++ b/packages/kbn-expandable-flyout/src/types.ts @@ -8,7 +8,7 @@ import React from 'react'; -export interface FlyoutPanel { +export interface FlyoutPanelProps { /** * Unique key to identify the panel */ @@ -35,5 +35,5 @@ export interface Panel { /** * Component to be rendered */ - component: (props: FlyoutPanel) => React.ReactElement; + component: (props: FlyoutPanelProps) => React.ReactElement; } diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx index d32b0ccfadf6..209608c260ec 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx @@ -1417,6 +1417,18 @@ describe('MetricVisComponent', function () { }); }); + it('does not override duration configuration at visualization level when set', () => { + getFormattedMetrics(394.2393, 983123.984, { + id: 'duration', + params: { formatOverride: true, outputFormat: 'asSeconds' }, + }); + expect(mockDeserialize).toHaveBeenCalledTimes(2); + expect(mockDeserialize).toHaveBeenCalledWith({ + id: 'duration', + params: { formatOverride: true, outputFormat: 'asSeconds' }, + }); + }); + it('does not tweak bytes format when passed', () => { getFormattedMetrics(394.2393, 983123.984, { id: 'bytes', diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx index 87154cd3dba7..f0aa3c3e1ded 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx @@ -49,7 +49,7 @@ export const defaultColor = euiThemeVars.euiColorLightestShade; function enhanceFieldFormat(serializedFieldFormat: SerializedFieldFormat | undefined) { const formatId = serializedFieldFormat?.id || 'number'; - if (formatId === 'duration') { + if (formatId === 'duration' && !serializedFieldFormat?.params?.formatOverride) { return { ...serializedFieldFormat, params: { diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx index 99b164433fe4..4da2d77825f4 100644 --- a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx @@ -12,7 +12,7 @@ import { mount, ReactWrapper } from 'enzyme'; import { I18nProvider } from '@kbn/i18n-react'; import { pluginServices } from '../services/plugin_services'; -import { DashboardListing, DashboardListingProps } from './dashboard_listing'; +import { DashboardListing } from './dashboard_listing'; /** * Mock Table List view. This dashboard component is a wrapper around the shared UX table List view. We @@ -20,6 +20,7 @@ import { DashboardListing, DashboardListingProps } from './dashboard_listing'; * in our tests because it is covered in its package. */ import { TableListView } from '@kbn/content-management-table-list-view'; +import { DashboardListingProps } from './types'; // import { TableListViewKibanaProvider } from '@kbn/content-management-table-list-view'; jest.mock('@kbn/content-management-table-list-view-table', () => { const originalModule = jest.requireActual('@kbn/content-management-table-list-view-table'); diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.tsx index 1890e9ca37e5..94f9460c8f25 100644 --- a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.tsx @@ -7,73 +7,25 @@ */ import { FormattedRelative, I18nProvider } from '@kbn/i18n-react'; -import React, { PropsWithChildren, useCallback, useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { type TableListViewKibanaDependencies, TableListViewKibanaProvider, - type UserContentCommonSchema, } from '@kbn/content-management-table-list-view-table'; import { TableListView } from '@kbn/content-management-table-list-view'; -import { ViewMode } from '@kbn/embeddable-plugin/public'; -import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; -import type { SavedObjectsFindOptionsReference } from '@kbn/core/public'; + import { toMountPoint, useExecutionContext } from '@kbn/kibana-react-plugin/public'; -import { - DASHBOARD_CONTENT_ID, - SAVED_OBJECT_DELETE_TIME, - SAVED_OBJECT_LOADED_TIME, -} from '../dashboard_constants'; -import { - dashboardListingTableStrings, - dashboardListingErrorStrings, -} from './_dashboard_listing_strings'; import { pluginServices } from '../services/plugin_services'; -import { confirmCreateWithUnsaved } from './confirm_overlays'; -import { DashboardItem } from '../../common/content_management'; -import { DashboardUnsavedListing } from './dashboard_unsaved_listing'; -import { DashboardApplicationService } from '../services/application/types'; -import { DashboardListingEmptyPrompt } from './dashboard_listing_empty_prompt'; - -// because the type of `application.capabilities.advancedSettings` is so generic, the provider -// requiring the `save` key to be part of it is causing type issues - so, creating a custom type -type TableListViewApplicationService = DashboardApplicationService & { - capabilities: { advancedSettings: { save: boolean } }; -}; - -const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit'; -const SAVED_OBJECTS_PER_PAGE_SETTING = 'savedObjects:perPage'; - -interface DashboardSavedObjectUserContent extends UserContentCommonSchema { - attributes: { - title: string; - description?: string; - timeRestore: boolean; - }; -} -const toTableListViewSavedObject = (hit: DashboardItem): DashboardSavedObjectUserContent => { - const { title, description, timeRestore } = hit.attributes; - return { - type: 'dashboard', - id: hit.id, - updatedAt: hit.updatedAt!, - references: hit.references, - attributes: { - title, - description, - timeRestore, - }, - }; -}; - -export type DashboardListingProps = PropsWithChildren<{ - initialFilter?: string; - useSessionStorageIntegration?: boolean; - goToDashboard: (dashboardId?: string, viewMode?: ViewMode) => void; - getDashboardUrl: (dashboardId: string, usesTimeRestore: boolean) => string; -}>; +import { DashboardUnsavedListing } from './dashboard_unsaved_listing'; +import { useDashboardListingTable } from './hooks/use_dashboard_listing_table'; +import { + DashboardListingProps, + DashboardSavedObjectUserContent, + TableListViewApplicationService, +} from './types'; export const DashboardListing = ({ children, @@ -89,123 +41,22 @@ export const DashboardListing = ({ http, chrome: { theme }, savedObjectsTagging, - dashboardSessionStorage, - settings: { uiSettings }, - notifications: { toasts }, + coreContext: { executionContext }, - dashboardCapabilities: { showWriteControls }, - dashboardContentManagement: { findDashboards, deleteDashboards }, } = pluginServices.getServices(); - const [unsavedDashboardIds, setUnsavedDashboardIds] = useState( - dashboardSessionStorage.getDashboardIdsWithUnsavedChanges() - ); - useExecutionContext(executionContext, { type: 'application', page: 'list', }); - const listingLimit = uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING); - const initialPageSize = uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING); - - const createItem = useCallback(() => { - if (useSessionStorageIntegration && dashboardSessionStorage.dashboardHasUnsavedEdits()) { - confirmCreateWithUnsaved(() => { - dashboardSessionStorage.clearState(); - goToDashboard(); - }, goToDashboard); - return; - } - goToDashboard(); - }, [dashboardSessionStorage, goToDashboard, useSessionStorageIntegration]); - - const fetchItems = useCallback( - ( - searchTerm: string, - { - references, - referencesToExclude, - }: { - references?: SavedObjectsFindOptionsReference[]; - referencesToExclude?: SavedObjectsFindOptionsReference[]; - } = {} - ) => { - const searchStartTime = window.performance.now(); - - return findDashboards - .search({ - search: searchTerm, - size: listingLimit, - hasReference: references, - hasNoReference: referencesToExclude, - }) - .then(({ total, hits }) => { - const searchEndTime = window.performance.now(); - const searchDuration = searchEndTime - searchStartTime; - reportPerformanceMetricEvent(pluginServices.getServices().analytics, { - eventName: SAVED_OBJECT_LOADED_TIME, - duration: searchDuration, - meta: { - saved_object_type: DASHBOARD_CONTENT_ID, - }, - }); - return { - total, - hits: hits.map(toTableListViewSavedObject), - }; - }); - }, - [findDashboards, listingLimit] - ); - - const deleteItems = useCallback( - async (dashboardsToDelete: Array<{ id: string }>) => { - try { - const deleteStartTime = window.performance.now(); - - await deleteDashboards( - dashboardsToDelete.map(({ id }) => { - dashboardSessionStorage.clearState(id); - return id; - }) - ); - - const deleteDuration = window.performance.now() - deleteStartTime; - reportPerformanceMetricEvent(pluginServices.getServices().analytics, { - eventName: SAVED_OBJECT_DELETE_TIME, - duration: deleteDuration, - meta: { - saved_object_type: DASHBOARD_CONTENT_ID, - total: dashboardsToDelete.length, - }, - }); - } catch (error) { - toasts.addError(error, { - title: dashboardListingErrorStrings.getErrorDeletingDashboardToast(), - }); - } - - setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()); - }, - [dashboardSessionStorage, deleteDashboards, toasts] - ); - - const editItem = useCallback( - ({ id }: { id: string | undefined }) => goToDashboard(id, ViewMode.EDIT), - [goToDashboard] - ); - const emptyPrompt = ( - - ); - - const { getEntityName, getTableListTitle, getEntityNamePlural } = dashboardListingTableStrings; + const { unsavedDashboardIds, refreshUnsavedDashboards, tableListViewTableProps } = + useDashboardListingTable({ + goToDashboard, + getDashboardUrl, + useSessionStorageIntegration, + initialFilter, + }); const savedObjectsTaggingFakePlugin = useMemo(() => { return savedObjectsTagging.hasApi // TODO: clean up this logic once https://github.com/elastic/kibana/issues/140433 is resolved @@ -231,32 +82,13 @@ export const DashboardListing = ({ FormattedRelative, }} > - - getDetailViewLink={({ id, attributes: { timeRestore } }) => - getDashboardUrl(id, timeRestore) - } - deleteItems={!showWriteControls ? undefined : deleteItems} - createItem={!showWriteControls ? undefined : createItem} - editItem={!showWriteControls ? undefined : editItem} - entityNamePlural={getEntityNamePlural()} - title={getTableListTitle()} - headingId="dashboardListingHeading" - initialPageSize={initialPageSize} - initialFilter={initialFilter} - entityName={getEntityName()} - listingLimit={listingLimit} - emptyPrompt={emptyPrompt} - findItems={fetchItems} - id="dashboard" - > + {...tableListViewTableProps}> <> {children} - setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()) - } + refreshUnsavedDashboards={refreshUnsavedDashboards} /> diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.test.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.test.tsx index 886d43a1db6d..255177975227 100644 --- a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.test.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.test.tsx @@ -34,6 +34,7 @@ const makeDefaultProps = (): DashboardListingEmptyPromptProps => ({ goToDashboard: jest.fn(), setUnsavedDashboardIds: jest.fn(), useSessionStorageIntegration: true, + disableCreateDashboardButton: false, }); function mountWith({ @@ -75,6 +76,21 @@ test('renders empty prompt with link when showWriteControls is on', async () => expect(component!.find('EuiLink').length).toBe(1); }); +test('renders disabled action button when disableCreateDashboardButton is true', async () => { + pluginServices.getServices().dashboardCapabilities.showWriteControls = true; + + let component: ReactWrapper; + await act(async () => { + ({ component } = mountWith({ props: { disableCreateDashboardButton: true } })); + }); + + component!.update(); + + expect(component!.find(`[data-test-subj="newItemButton"]`).first().prop('disabled')).toEqual( + true + ); +}); + test('renders continue button when no dashboards exist but one is in progress', async () => { pluginServices.getServices().dashboardCapabilities.showWriteControls = true; let component: ReactWrapper; diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx index a518c520bcbd..d1460c53f23e 100644 --- a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx @@ -22,13 +22,14 @@ import { getNewDashboardTitle, dashboardUnsavedListingStrings, } from './_dashboard_listing_strings'; -import { DashboardListingProps } from './dashboard_listing'; import { pluginServices } from '../services/plugin_services'; import { confirmDiscardUnsavedChanges } from './confirm_overlays'; import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_session_storage/dashboard_session_storage_service'; +import { DashboardListingProps } from './types'; export interface DashboardListingEmptyPromptProps { createItem: () => void; + disableCreateDashboardButton?: boolean; unsavedDashboardIds: string[]; goToDashboard: DashboardListingProps['goToDashboard']; setUnsavedDashboardIds: React.Dispatch>; @@ -41,6 +42,7 @@ export const DashboardListingEmptyPrompt = ({ unsavedDashboardIds, goToDashboard, createItem, + disableCreateDashboardButton, }: DashboardListingEmptyPromptProps) => { const { application, @@ -56,7 +58,13 @@ export const DashboardListingEmptyPrompt = ({ const getEmptyAction = useCallback(() => { if (!isEditingFirstDashboard) { return ( - + {noItemsStrings.getCreateNewDashboardText()} ); @@ -94,11 +102,12 @@ export const DashboardListingEmptyPrompt = ({ ); }, [ - dashboardSessionStorage, isEditingFirstDashboard, + createItem, + disableCreateDashboardButton, + dashboardSessionStorage, setUnsavedDashboardIds, goToDashboard, - createItem, ]); if (!showWriteControls) { diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_table.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_table.tsx new file mode 100644 index 000000000000..196fd04cddf6 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_table.tsx @@ -0,0 +1,112 @@ +/* + * 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 { FormattedRelative, I18nProvider } from '@kbn/i18n-react'; +import React, { useMemo } from 'react'; + +import { + type TableListViewKibanaDependencies, + TableListViewKibanaProvider, + TableListViewTable, +} from '@kbn/content-management-table-list-view-table'; + +import { toMountPoint, useExecutionContext } from '@kbn/kibana-react-plugin/public'; + +import { pluginServices } from '../services/plugin_services'; + +import { DashboardUnsavedListing } from './dashboard_unsaved_listing'; +import { useDashboardListingTable } from './hooks/use_dashboard_listing_table'; +import { + DashboardListingProps, + DashboardSavedObjectUserContent, + TableListViewApplicationService, +} from './types'; + +export const DashboardListingTable = ({ + disableCreateDashboardButton, + initialFilter, + goToDashboard, + getDashboardUrl, + useSessionStorageIntegration, + urlStateEnabled, +}: DashboardListingProps) => { + const { + application, + notifications, + overlays, + http, + savedObjectsTagging, + coreContext: { executionContext }, + chrome: { theme }, + } = pluginServices.getServices(); + + useExecutionContext(executionContext, { + type: 'application', + page: 'list', + }); + + const { + unsavedDashboardIds, + refreshUnsavedDashboards, + tableListViewTableProps: { title: tableCaption, ...tableListViewTable }, + } = useDashboardListingTable({ + disableCreateDashboardButton, + goToDashboard, + getDashboardUrl, + urlStateEnabled, + useSessionStorageIntegration, + initialFilter, + }); + + const savedObjectsTaggingFakePlugin = useMemo( + () => + savedObjectsTagging.hasApi // TODO: clean up this logic once https://github.com/elastic/kibana/issues/140433 is resolved + ? ({ + ui: savedObjectsTagging, + } as TableListViewKibanaDependencies['savedObjectsTagging']) + : undefined, + [savedObjectsTagging] + ); + + const core = useMemo( + () => ({ + application: application as TableListViewApplicationService, + notifications, + overlays, + http, + theme, + }), + [application, notifications, overlays, http, theme] + ); + + return ( + + + <> + + + tableCaption={tableCaption} + {...tableListViewTable} + /> + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default DashboardListingTable; diff --git a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx new file mode 100644 index 000000000000..ceb53af4bf2e --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx @@ -0,0 +1,247 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; + +import { useDashboardListingTable } from './use_dashboard_listing_table'; +import { pluginServices } from '../../services/plugin_services'; +import { confirmCreateWithUnsaved } from '../confirm_overlays'; +import { DashboardSavedObjectUserContent } from '../types'; +const clearStateMock = jest.fn(); +const getDashboardUrl = jest.fn(); +const goToDashboard = jest.fn(); +const deleteDashboards = jest.fn().mockResolvedValue(true); +const getUiSettingsMock = jest.fn().mockImplementation((key) => { + if (key === 'savedObjects:listingLimit') { + return 20; + } + if (key === 'savedObjects:perPage') { + return 5; + } + return null; +}); +const getPluginServices = pluginServices.getServices(); + +jest.mock('@kbn/ebt-tools', () => ({ + reportPerformanceMetricEvent: jest.fn(), +})); + +jest.mock('../confirm_overlays', () => ({ + confirmCreateWithUnsaved: jest.fn().mockImplementation((fn) => fn()), +})); + +jest.mock('../_dashboard_listing_strings', () => ({ + dashboardListingTableStrings: { + getEntityName: jest.fn().mockReturnValue('Dashboard'), + getTableListTitle: jest.fn().mockReturnValue('Dashboard List'), + getEntityNamePlural: jest.fn().mockReturnValue('Dashboards'), + }, +})); + +describe('useDashboardListingTable', () => { + beforeEach(() => { + jest.clearAllMocks(); + + getPluginServices.dashboardSessionStorage.dashboardHasUnsavedEdits = jest + .fn() + .mockReturnValue(true); + + getPluginServices.dashboardSessionStorage.getDashboardIdsWithUnsavedChanges = jest + .fn() + .mockReturnValue([]); + + getPluginServices.dashboardSessionStorage.clearState = clearStateMock; + getPluginServices.dashboardCapabilities.showWriteControls = true; + getPluginServices.dashboardContentManagement.deleteDashboards = deleteDashboards; + getPluginServices.settings.uiSettings.get = getUiSettingsMock; + getPluginServices.notifications.toasts.addError = jest.fn(); + }); + + test('should return the correct initial hasInitialFetchReturned state', () => { + const { result } = renderHook(() => + useDashboardListingTable({ + getDashboardUrl, + goToDashboard, + }) + ); + + expect(result.current.hasInitialFetchReturned).toBe(false); + }); + + test('should return the correct initial pageDataTestSubject state', () => { + const { result } = renderHook(() => + useDashboardListingTable({ + getDashboardUrl, + goToDashboard, + }) + ); + + expect(result.current.pageDataTestSubject).toBeUndefined(); + }); + + test('should return the correct refreshUnsavedDashboards function', () => { + const { result } = renderHook(() => + useDashboardListingTable({ + getDashboardUrl, + goToDashboard, + }) + ); + + expect(typeof result.current.refreshUnsavedDashboards).toBe('function'); + }); + + test('should return the correct initial unsavedDashboardIds state', () => { + const { result } = renderHook(() => + useDashboardListingTable({ + getDashboardUrl, + goToDashboard, + }) + ); + + expect(result.current.unsavedDashboardIds).toEqual([]); + }); + + test('should return the correct tableListViewTableProps', () => { + const initialFilter = 'myFilter'; + const { result } = renderHook(() => + useDashboardListingTable({ + getDashboardUrl, + goToDashboard, + initialFilter, + urlStateEnabled: false, + }) + ); + + const tableListViewTableProps = result.current.tableListViewTableProps; + + const expectedProps = { + createItem: expect.any(Function), + deleteItems: expect.any(Function), + editItem: expect.any(Function), + emptyPrompt: expect.any(Object), + entityName: 'Dashboard', + entityNamePlural: 'Dashboards', + findItems: expect.any(Function), + getDetailViewLink: expect.any(Function), + headingId: 'dashboardListingHeading', + id: expect.any(String), + initialFilter: 'myFilter', + initialPageSize: 5, + listingLimit: 20, + onFetchSuccess: expect.any(Function), + setPageDataTestSubject: expect.any(Function), + title: 'Dashboard List', + urlStateEnabled: false, + }; + + expect(tableListViewTableProps).toEqual(expectedProps); + }); + + test('should call deleteDashboards when deleteItems is called', () => { + const { result } = renderHook(() => + useDashboardListingTable({ + getDashboardUrl, + goToDashboard, + }) + ); + + act(() => { + result.current.tableListViewTableProps.deleteItems?.([ + { id: 'test-id' } as DashboardSavedObjectUserContent, + ]); + }); + + expect(deleteDashboards).toHaveBeenCalled(); + }); + + test('should call goToDashboard when editItem is called', () => { + const { result } = renderHook(() => + useDashboardListingTable({ + getDashboardUrl, + goToDashboard, + }) + ); + + act(() => { + result.current.tableListViewTableProps.editItem?.({ + id: 'test-id', + } as DashboardSavedObjectUserContent); + }); + + expect(goToDashboard).toHaveBeenCalled(); + }); + + test('should call goToDashboard when createItem is called without unsaved changes', () => { + const { result } = renderHook(() => + useDashboardListingTable({ + getDashboardUrl, + goToDashboard, + }) + ); + + act(() => { + result.current.tableListViewTableProps.createItem?.(); + }); + + expect(goToDashboard).toHaveBeenCalled(); + }); + + test('should call confirmCreateWithUnsaved and clear state when createItem is called with unsaved changes', () => { + const { result } = renderHook(() => + useDashboardListingTable({ + getDashboardUrl, + goToDashboard, + useSessionStorageIntegration: true, + }) + ); + + act(() => { + result.current.tableListViewTableProps.createItem?.(); + }); + + expect(confirmCreateWithUnsaved).toHaveBeenCalled(); + expect(clearStateMock).toHaveBeenCalled(); + expect(goToDashboard).toHaveBeenCalled(); + }); + + test('createItem should be undefined when showWriteControls equals false', () => { + getPluginServices.dashboardCapabilities.showWriteControls = false; + + const { result } = renderHook(() => + useDashboardListingTable({ + getDashboardUrl, + goToDashboard, + }) + ); + + expect(result.current.tableListViewTableProps.createItem).toBeUndefined(); + }); + + test('deleteItems should be undefined when showWriteControls equals false', () => { + getPluginServices.dashboardCapabilities.showWriteControls = false; + const { result } = renderHook(() => + useDashboardListingTable({ + getDashboardUrl, + goToDashboard, + }) + ); + + expect(result.current.tableListViewTableProps.deleteItems).toBeUndefined(); + }); + + test('editItem should be undefined when showWriteControls equals false', () => { + getPluginServices.dashboardCapabilities.showWriteControls = false; + const { result } = renderHook(() => + useDashboardListingTable({ + getDashboardUrl, + goToDashboard, + }) + ); + + expect(result.current.tableListViewTableProps.editItem).toBeUndefined(); + }); +}); diff --git a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx new file mode 100644 index 000000000000..f95f2649673b --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx @@ -0,0 +1,273 @@ +/* + * 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, { useCallback, useState, useMemo } from 'react'; +import type { SavedObjectsFindOptionsReference } from '@kbn/core/public'; +import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; +import { TableListViewTableProps } from '@kbn/content-management-table-list-view-table'; +import { ViewMode } from '@kbn/embeddable-plugin/public'; + +import { DashboardListingEmptyPrompt } from '../dashboard_listing_empty_prompt'; +import { pluginServices } from '../../services/plugin_services'; +import { + DASHBOARD_CONTENT_ID, + SAVED_OBJECT_DELETE_TIME, + SAVED_OBJECT_LOADED_TIME, +} from '../../dashboard_constants'; +import { DashboardItem } from '../../../common/content_management'; +import { + dashboardListingErrorStrings, + dashboardListingTableStrings, +} from '../_dashboard_listing_strings'; +import { confirmCreateWithUnsaved } from '../confirm_overlays'; +import { DashboardSavedObjectUserContent } from '../types'; + +type GetDetailViewLink = + TableListViewTableProps['getDetailViewLink']; + +const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit'; +const SAVED_OBJECTS_PER_PAGE_SETTING = 'savedObjects:perPage'; + +const toTableListViewSavedObject = (hit: DashboardItem): DashboardSavedObjectUserContent => { + const { title, description, timeRestore } = hit.attributes; + return { + type: 'dashboard', + id: hit.id, + updatedAt: hit.updatedAt!, + references: hit.references, + attributes: { + title, + description, + timeRestore, + }, + }; +}; + +interface UseDashboardListingTableReturnType { + hasInitialFetchReturned: boolean; + pageDataTestSubject: string | undefined; + refreshUnsavedDashboards: () => void; + tableListViewTableProps: Omit< + TableListViewTableProps, + 'tableCaption' + > & { title: string }; + unsavedDashboardIds: string[]; +} + +export const useDashboardListingTable = ({ + dashboardListingId = 'dashboard', + disableCreateDashboardButton, + getDashboardUrl, + goToDashboard, + headingId = 'dashboardListingHeading', + initialFilter, + urlStateEnabled, + useSessionStorageIntegration, +}: { + dashboardListingId?: string; + disableCreateDashboardButton?: boolean; + getDashboardUrl: (dashboardId: string, usesTimeRestore: boolean) => string; + goToDashboard: (dashboardId?: string, viewMode?: ViewMode) => void; + headingId?: string; + initialFilter?: string; + urlStateEnabled?: boolean; + useSessionStorageIntegration?: boolean; +}): UseDashboardListingTableReturnType => { + const { + dashboardSessionStorage, + dashboardCapabilities: { showWriteControls }, + dashboardContentManagement: { findDashboards, deleteDashboards }, + settings: { uiSettings }, + notifications: { toasts }, + } = pluginServices.getServices(); + + const { getEntityName, getTableListTitle, getEntityNamePlural } = dashboardListingTableStrings; + const title = getTableListTitle(); + const entityName = getEntityName(); + const entityNamePlural = getEntityNamePlural(); + const [pageDataTestSubject, setPageDataTestSubject] = useState(); + const [hasInitialFetchReturned, setHasInitialFetchReturned] = useState(false); + const [unsavedDashboardIds, setUnsavedDashboardIds] = useState( + dashboardSessionStorage.getDashboardIdsWithUnsavedChanges() + ); + + const listingLimit = uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING); + const initialPageSize = uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING); + + const createItem = useCallback(() => { + if (useSessionStorageIntegration && dashboardSessionStorage.dashboardHasUnsavedEdits()) { + confirmCreateWithUnsaved(() => { + dashboardSessionStorage.clearState(); + goToDashboard(); + }, goToDashboard); + return; + } + goToDashboard(); + }, [dashboardSessionStorage, goToDashboard, useSessionStorageIntegration]); + + const emptyPrompt = useMemo( + () => ( + + ), + [ + createItem, + disableCreateDashboardButton, + goToDashboard, + unsavedDashboardIds, + useSessionStorageIntegration, + ] + ); + + const findItems = useCallback( + ( + searchTerm: string, + { + references, + referencesToExclude, + }: { + references?: SavedObjectsFindOptionsReference[]; + referencesToExclude?: SavedObjectsFindOptionsReference[]; + } = {} + ) => { + const searchStartTime = window.performance.now(); + + return findDashboards + .search({ + search: searchTerm, + size: listingLimit, + hasReference: references, + hasNoReference: referencesToExclude, + }) + .then(({ total, hits }) => { + const searchEndTime = window.performance.now(); + const searchDuration = searchEndTime - searchStartTime; + reportPerformanceMetricEvent(pluginServices.getServices().analytics, { + eventName: SAVED_OBJECT_LOADED_TIME, + duration: searchDuration, + meta: { + saved_object_type: DASHBOARD_CONTENT_ID, + }, + }); + return { + total, + hits: hits.map(toTableListViewSavedObject), + }; + }); + }, + [findDashboards, listingLimit] + ); + + const deleteItems = useCallback( + async (dashboardsToDelete: Array<{ id: string }>) => { + try { + const deleteStartTime = window.performance.now(); + + await deleteDashboards( + dashboardsToDelete.map(({ id }) => { + dashboardSessionStorage.clearState(id); + return id; + }) + ); + + const deleteDuration = window.performance.now() - deleteStartTime; + reportPerformanceMetricEvent(pluginServices.getServices().analytics, { + eventName: SAVED_OBJECT_DELETE_TIME, + duration: deleteDuration, + meta: { + saved_object_type: DASHBOARD_CONTENT_ID, + total: dashboardsToDelete.length, + }, + }); + } catch (error) { + toasts.addError(error, { + title: dashboardListingErrorStrings.getErrorDeletingDashboardToast(), + }); + } + + setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()); + }, + [dashboardSessionStorage, deleteDashboards, toasts] + ); + + const editItem = useCallback( + ({ id }: { id: string | undefined }) => goToDashboard(id, ViewMode.EDIT), + [goToDashboard] + ); + + const onFetchSuccess = useCallback(() => { + if (!hasInitialFetchReturned) { + setHasInitialFetchReturned(true); + } + }, [hasInitialFetchReturned]); + + const getDetailViewLink: GetDetailViewLink = useCallback( + ({ id, attributes: { timeRestore } }) => getDashboardUrl(id, timeRestore), + [getDashboardUrl] + ); + + const tableListViewTableProps = useMemo( + () => ({ + createItem: !showWriteControls ? undefined : createItem, + deleteItems: !showWriteControls ? undefined : deleteItems, + editItem: !showWriteControls ? undefined : editItem, + emptyPrompt, + entityName, + entityNamePlural, + findItems, + getDetailViewLink, + headingId, + id: dashboardListingId, + initialFilter, + initialPageSize, + listingLimit, + onFetchSuccess, + setPageDataTestSubject, + title, + urlStateEnabled, + }), + [ + createItem, + dashboardListingId, + deleteItems, + editItem, + emptyPrompt, + entityName, + entityNamePlural, + findItems, + getDetailViewLink, + headingId, + initialFilter, + initialPageSize, + listingLimit, + onFetchSuccess, + showWriteControls, + title, + urlStateEnabled, + ] + ); + + const refreshUnsavedDashboards = useCallback( + () => setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()), + [dashboardSessionStorage] + ); + + return { + hasInitialFetchReturned, + pageDataTestSubject, + refreshUnsavedDashboards, + tableListViewTableProps, + unsavedDashboardIds, + }; +}; diff --git a/src/plugins/dashboard/public/dashboard_listing/index.tsx b/src/plugins/dashboard/public/dashboard_listing/index.tsx index 92febf2904bd..c1c901225283 100644 --- a/src/plugins/dashboard/public/dashboard_listing/index.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/index.tsx @@ -10,7 +10,7 @@ import React, { Suspense } from 'react'; import { EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; import { servicesReady } from '../plugin'; -import { DashboardListingProps } from './dashboard_listing'; +import { DashboardListingProps } from './types'; const ListingTableLoadingIndicator = () => { return } />; @@ -18,11 +18,11 @@ const ListingTableLoadingIndicator = () => { const LazyDashboardListing = React.lazy(() => (async () => { - const modulePromise = import('./dashboard_listing'); + const modulePromise = import('./dashboard_listing_table'); const [module] = await Promise.all([modulePromise, servicesReady]); return { - default: module.DashboardListing, + default: module.DashboardListingTable, }; })().then((module) => module) ); diff --git a/src/plugins/dashboard/public/dashboard_listing/types.ts b/src/plugins/dashboard/public/dashboard_listing/types.ts new file mode 100644 index 000000000000..c92344d4e778 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_listing/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { PropsWithChildren } from 'react'; +import { type UserContentCommonSchema } from '@kbn/content-management-table-list-view-table'; +import { ViewMode } from '@kbn/embeddable-plugin/public'; +import { DashboardApplicationService } from '../services/application/types'; + +export type DashboardListingProps = PropsWithChildren<{ + disableCreateDashboardButton?: boolean; + initialFilter?: string; + useSessionStorageIntegration?: boolean; + goToDashboard: (dashboardId?: string, viewMode?: ViewMode) => void; + getDashboardUrl: (dashboardId: string, usesTimeRestore: boolean) => string; + urlStateEnabled?: boolean; +}>; + +// because the type of `application.capabilities.advancedSettings` is so generic, the provider +// requiring the `save` key to be part of it is causing type issues - so, creating a custom type +export type TableListViewApplicationService = DashboardApplicationService & { + capabilities: { advancedSettings: { save: boolean } }; +}; + +export interface DashboardSavedObjectUserContent extends UserContentCommonSchema { + attributes: { + title: string; + description?: string; + timeRestore: boolean; + }; +} diff --git a/src/plugins/field_formats/common/constants/duration_formats.ts b/src/plugins/field_formats/common/constants/duration_formats.ts new file mode 100644 index 000000000000..ab83b71c276b --- /dev/null +++ b/src/plugins/field_formats/common/constants/duration_formats.ts @@ -0,0 +1,171 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +const DEFAULT_INPUT_FORMAT = { + text: i18n.translate('fieldFormats.duration.inputFormats.seconds', { + defaultMessage: 'Seconds', + }), + kind: 'seconds', +}; +const inputFormats = [ + { + text: i18n.translate('fieldFormats.duration.inputFormats.picoseconds', { + defaultMessage: 'Picoseconds', + }), + kind: 'picoseconds', + }, + { + text: i18n.translate('fieldFormats.duration.inputFormats.nanoseconds', { + defaultMessage: 'Nanoseconds', + }), + kind: 'nanoseconds', + }, + { + text: i18n.translate('fieldFormats.duration.inputFormats.microseconds', { + defaultMessage: 'Microseconds', + }), + kind: 'microseconds', + }, + { + text: i18n.translate('fieldFormats.duration.inputFormats.milliseconds', { + defaultMessage: 'Milliseconds', + }), + kind: 'milliseconds', + }, + { ...DEFAULT_INPUT_FORMAT }, + { + text: i18n.translate('fieldFormats.duration.inputFormats.minutes', { + defaultMessage: 'Minutes', + }), + kind: 'minutes', + }, + { + text: i18n.translate('fieldFormats.duration.inputFormats.hours', { + defaultMessage: 'Hours', + }), + kind: 'hours', + }, + { + text: i18n.translate('fieldFormats.duration.inputFormats.days', { + defaultMessage: 'Days', + }), + kind: 'days', + }, + { + text: i18n.translate('fieldFormats.duration.inputFormats.weeks', { + defaultMessage: 'Weeks', + }), + kind: 'weeks', + }, + { + text: i18n.translate('fieldFormats.duration.inputFormats.months', { + defaultMessage: 'Months', + }), + kind: 'months', + }, + { + text: i18n.translate('fieldFormats.duration.inputFormats.years', { + defaultMessage: 'Years', + }), + kind: 'years', + }, +]; +const DEFAULT_OUTPUT_FORMAT = { + text: i18n.translate('fieldFormats.duration.outputFormats.humanize.approximate', { + defaultMessage: 'Human-readable (approximate)', + }), + method: 'humanize', +}; +const outputFormats = [ + { ...DEFAULT_OUTPUT_FORMAT }, + { + text: i18n.translate('fieldFormats.duration.outputFormats.humanize.precise', { + defaultMessage: 'Human-readable (precise)', + }), + method: 'humanizePrecise', + }, + { + text: i18n.translate('fieldFormats.duration.outputFormats.asMilliseconds', { + defaultMessage: 'Milliseconds', + }), + shortText: i18n.translate('fieldFormats.duration.outputFormats.asMilliseconds.short', { + defaultMessage: 'ms', + }), + method: 'asMilliseconds', + }, + { + text: i18n.translate('fieldFormats.duration.outputFormats.asSeconds', { + defaultMessage: 'Seconds', + }), + shortText: i18n.translate('fieldFormats.duration.outputFormats.asSeconds.short', { + defaultMessage: 's', + }), + method: 'asSeconds', + }, + { + text: i18n.translate('fieldFormats.duration.outputFormats.asMinutes', { + defaultMessage: 'Minutes', + }), + shortText: i18n.translate('fieldFormats.duration.outputFormats.asMinutes.short', { + defaultMessage: 'min', + }), + method: 'asMinutes', + }, + { + text: i18n.translate('fieldFormats.duration.outputFormats.asHours', { + defaultMessage: 'Hours', + }), + shortText: i18n.translate('fieldFormats.duration.outputFormats.asHours.short', { + defaultMessage: 'h', + }), + method: 'asHours', + }, + { + text: i18n.translate('fieldFormats.duration.outputFormats.asDays', { + defaultMessage: 'Days', + }), + shortText: i18n.translate('fieldFormats.duration.outputFormats.asDays.short', { + defaultMessage: 'd', + }), + method: 'asDays', + }, + { + text: i18n.translate('fieldFormats.duration.outputFormats.asWeeks', { + defaultMessage: 'Weeks', + }), + shortText: i18n.translate('fieldFormats.duration.outputFormats.asWeeks.short', { + defaultMessage: 'w', + }), + method: 'asWeeks', + }, + { + text: i18n.translate('fieldFormats.duration.outputFormats.asMonths', { + defaultMessage: 'Months', + }), + shortText: i18n.translate('fieldFormats.duration.outputFormats.asMonths.short', { + defaultMessage: 'mon', + }), + method: 'asMonths', + }, + { + text: i18n.translate('fieldFormats.duration.outputFormats.asYears', { + defaultMessage: 'Years', + }), + shortText: i18n.translate('fieldFormats.duration.outputFormats.asYears.short', { + defaultMessage: 'y', + }), + method: 'asYears', + }, +]; + +export const DEFAULT_DURATION_INPUT_FORMAT = DEFAULT_INPUT_FORMAT; +export const DEFAULT_DURATION_OUTPUT_FORMAT = DEFAULT_OUTPUT_FORMAT; +export const DURATION_INPUT_FORMATS = inputFormats; +export const DURATION_OUTPUT_FORMATS = outputFormats; diff --git a/src/plugins/field_formats/common/converters/duration.ts b/src/plugins/field_formats/common/converters/duration.ts index 72f893b59ef4..1579d6058e98 100644 --- a/src/plugins/field_formats/common/converters/duration.ts +++ b/src/plugins/field_formats/common/converters/duration.ts @@ -11,171 +11,22 @@ import moment, { unitOfTime, Duration } from 'moment'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { FieldFormat } from '../field_format'; import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; +import { + DEFAULT_DURATION_INPUT_FORMAT, + DEFAULT_DURATION_OUTPUT_FORMAT, + DURATION_INPUT_FORMATS, + DURATION_OUTPUT_FORMATS, +} from '../constants/duration_formats'; const ratioToSeconds: Record = { picoseconds: 0.000000000001, nanoseconds: 0.000000001, microseconds: 0.000001, }; + const HUMAN_FRIENDLY = 'humanize'; const HUMAN_FRIENDLY_PRECISE = 'humanizePrecise'; const DEFAULT_OUTPUT_PRECISION = 2; -const DEFAULT_INPUT_FORMAT = { - text: i18n.translate('fieldFormats.duration.inputFormats.seconds', { - defaultMessage: 'Seconds', - }), - kind: 'seconds', -}; -const inputFormats = [ - { - text: i18n.translate('fieldFormats.duration.inputFormats.picoseconds', { - defaultMessage: 'Picoseconds', - }), - kind: 'picoseconds', - }, - { - text: i18n.translate('fieldFormats.duration.inputFormats.nanoseconds', { - defaultMessage: 'Nanoseconds', - }), - kind: 'nanoseconds', - }, - { - text: i18n.translate('fieldFormats.duration.inputFormats.microseconds', { - defaultMessage: 'Microseconds', - }), - kind: 'microseconds', - }, - { - text: i18n.translate('fieldFormats.duration.inputFormats.milliseconds', { - defaultMessage: 'Milliseconds', - }), - kind: 'milliseconds', - }, - { ...DEFAULT_INPUT_FORMAT }, - { - text: i18n.translate('fieldFormats.duration.inputFormats.minutes', { - defaultMessage: 'Minutes', - }), - kind: 'minutes', - }, - { - text: i18n.translate('fieldFormats.duration.inputFormats.hours', { - defaultMessage: 'Hours', - }), - kind: 'hours', - }, - { - text: i18n.translate('fieldFormats.duration.inputFormats.days', { - defaultMessage: 'Days', - }), - kind: 'days', - }, - { - text: i18n.translate('fieldFormats.duration.inputFormats.weeks', { - defaultMessage: 'Weeks', - }), - kind: 'weeks', - }, - { - text: i18n.translate('fieldFormats.duration.inputFormats.months', { - defaultMessage: 'Months', - }), - kind: 'months', - }, - { - text: i18n.translate('fieldFormats.duration.inputFormats.years', { - defaultMessage: 'Years', - }), - kind: 'years', - }, -]; -const DEFAULT_OUTPUT_FORMAT = { - text: i18n.translate('fieldFormats.duration.outputFormats.humanize.approximate', { - defaultMessage: 'Human-readable (approximate)', - }), - method: 'humanize', -}; -const outputFormats = [ - { ...DEFAULT_OUTPUT_FORMAT }, - { - text: i18n.translate('fieldFormats.duration.outputFormats.humanize.precise', { - defaultMessage: 'Human-readable (precise)', - }), - method: 'humanizePrecise', - }, - { - text: i18n.translate('fieldFormats.duration.outputFormats.asMilliseconds', { - defaultMessage: 'Milliseconds', - }), - shortText: i18n.translate('fieldFormats.duration.outputFormats.asMilliseconds.short', { - defaultMessage: 'ms', - }), - method: 'asMilliseconds', - }, - { - text: i18n.translate('fieldFormats.duration.outputFormats.asSeconds', { - defaultMessage: 'Seconds', - }), - shortText: i18n.translate('fieldFormats.duration.outputFormats.asSeconds.short', { - defaultMessage: 's', - }), - method: 'asSeconds', - }, - { - text: i18n.translate('fieldFormats.duration.outputFormats.asMinutes', { - defaultMessage: 'Minutes', - }), - shortText: i18n.translate('fieldFormats.duration.outputFormats.asMinutes.short', { - defaultMessage: 'min', - }), - method: 'asMinutes', - }, - { - text: i18n.translate('fieldFormats.duration.outputFormats.asHours', { - defaultMessage: 'Hours', - }), - shortText: i18n.translate('fieldFormats.duration.outputFormats.asHours.short', { - defaultMessage: 'h', - }), - method: 'asHours', - }, - { - text: i18n.translate('fieldFormats.duration.outputFormats.asDays', { - defaultMessage: 'Days', - }), - shortText: i18n.translate('fieldFormats.duration.outputFormats.asDays.short', { - defaultMessage: 'd', - }), - method: 'asDays', - }, - { - text: i18n.translate('fieldFormats.duration.outputFormats.asWeeks', { - defaultMessage: 'Weeks', - }), - shortText: i18n.translate('fieldFormats.duration.outputFormats.asWeeks.short', { - defaultMessage: 'w', - }), - method: 'asWeeks', - }, - { - text: i18n.translate('fieldFormats.duration.outputFormats.asMonths', { - defaultMessage: 'Months', - }), - shortText: i18n.translate('fieldFormats.duration.outputFormats.asMonths.short', { - defaultMessage: 'mon', - }), - method: 'asMonths', - }, - { - text: i18n.translate('fieldFormats.duration.outputFormats.asYears', { - defaultMessage: 'Years', - }), - shortText: i18n.translate('fieldFormats.duration.outputFormats.asYears.short', { - defaultMessage: 'y', - }), - method: 'asYears', - }, -]; function parseInputAsDuration(val: number, inputFormat: string) { const ratio = ratioToSeconds[inputFormat] || 1; @@ -214,8 +65,8 @@ export class DurationFormat extends FieldFormat { defaultMessage: 'Duration', }); static fieldType = KBN_FIELD_TYPES.NUMBER; - static inputFormats = inputFormats; - static outputFormats = outputFormats; + static inputFormats = DURATION_INPUT_FORMATS; + static outputFormats = DURATION_OUTPUT_FORMATS; allowsNumericalAggregations = true; isHuman() { @@ -228,8 +79,8 @@ export class DurationFormat extends FieldFormat { getParamDefaults() { return { - inputFormat: DEFAULT_INPUT_FORMAT.kind, - outputFormat: DEFAULT_OUTPUT_FORMAT.method, + inputFormat: DEFAULT_DURATION_INPUT_FORMAT.kind, + outputFormat: DEFAULT_DURATION_OUTPUT_FORMAT.method, outputPrecision: DEFAULT_OUTPUT_PRECISION, includeSpaceWithSuffix: true, }; @@ -261,7 +112,7 @@ export class DurationFormat extends FieldFormat { : duration[outputFormat](); const precise = human || humanPrecise ? formatted : formatted.toFixed(outputPrecision); - const type = outputFormats.find(({ method }) => method === outputFormat); + const type = DURATION_OUTPUT_FORMATS.find(({ method }) => method === outputFormat); const unitText = useShortSuffix ? type?.shortText : type?.text.toLowerCase(); @@ -293,7 +144,7 @@ function formatDuration( ]; const getUnitText = (method: string) => { - const type = outputFormats.find(({ method: methodT }) => method === methodT); + const type = DURATION_OUTPUT_FORMATS.find(({ method: methodT }) => method === methodT); return useShortSuffix ? type?.shortText : type?.text.toLowerCase(); }; diff --git a/src/plugins/field_formats/common/index.ts b/src/plugins/field_formats/common/index.ts index 9f3a037d5f5e..23c6f51b0482 100644 --- a/src/plugins/field_formats/common/index.ts +++ b/src/plugins/field_formats/common/index.ts @@ -37,6 +37,12 @@ export { getHighlightRequest, geoUtils } from './utils'; export { DEFAULT_CONVERTER_COLOR } from './constants/color_default'; export { FORMATS_UI_SETTINGS } from './constants/ui_settings'; +export { + DEFAULT_DURATION_INPUT_FORMAT, + DEFAULT_DURATION_OUTPUT_FORMAT, + DURATION_INPUT_FORMATS, + DURATION_OUTPUT_FORMATS, +} from './constants/duration_formats'; export { FIELD_FORMAT_IDS } from './types'; export { HTML_CONTEXT_TYPE, TEXT_CONTEXT_TYPE } from './content_types'; diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json index f7015ee20251..2acf9f6bbdd1 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json @@ -10,7 +10,6 @@ "enabled": true }, "type": "test-is-exportable", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z", "references": [ { @@ -40,7 +39,6 @@ "enabled": false }, "type": "test-is-exportable", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z", "references": [] } @@ -59,7 +57,6 @@ "enabled": true }, "type": "test-is-exportable", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z", "references": [ { @@ -89,7 +86,6 @@ "enabled": false }, "type": "test-is-exportable", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z", "references": [] } @@ -108,7 +104,6 @@ "enabled": true }, "type": "test-is-exportable", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z", "references": [] } @@ -127,7 +122,6 @@ "enabled": true }, "type": "test-is-exportable", - "migrationVersion": {}, "updated_at": "2018-12-21T00:43:07.096Z", "references": [] } diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/mappings.json deleted file mode 100644 index d0101f16f85d..000000000000 --- a/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/mappings.json +++ /dev/null @@ -1,464 +0,0 @@ -{ - "type": "index", - "value": { - "index": ".kibana", - "settings": { - "index": { - "number_of_shards": "1", - "auto_expand_replicas": "0-1", - "number_of_replicas": "0" - } - }, - "mappings": { - "dynamic": "strict", - "properties": { - "test-export-transform": { - "properties": { - "title": { "type": "text" }, - "enabled": { "type": "boolean" } - } - }, - "test-is-exportable": { - "properties": { - "title": { "type": "text" }, - "enabled": { "type": "boolean" } - } - }, - "test-export-add": { - "properties": { - "title": { "type": "text" } - } - }, - "test-export-add-dep": { - "properties": { - "title": { "type": "text" } - } - }, - "test-export-transform-error": { - "properties": { - "title": { "type": "text" } - } - }, - "test-export-invalid-transform": { - "properties": { - "title": { "type": "text" } - } - }, - "apm-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "go": { - "type": "long", - "null_value": 0 - }, - "java": { - "type": "long", - "null_value": 0 - }, - "js-base": { - "type": "long", - "null_value": 0 - }, - "nodejs": { - "type": "long", - "null_value": 0 - }, - "python": { - "type": "long", - "null_value": 0 - }, - "ruby": { - "type": "long", - "null_value": 0 - } - } - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "id": { - "type": "text", - "index": false - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "accessibility:disableAnimations": { - "type": "boolean" - }, - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "defaultIndex": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "telemetry:optIn": { - "type": "boolean" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "map": { - "properties": { - "bounds": { - "dynamic": false, - "properties": {} - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "space": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "initials": { - "type": "keyword" - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "spaceId": { - "type": "keyword" - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - } - } - } - } -} diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/data.json index 54feabcf5433..0c0b1b419a14 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/data.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/data.json @@ -5,8 +5,7 @@ "index": ".kibana", "source": { "coreMigrationVersion": "7.14.0", - "references": [ - ], + "references": [], "test-export-transform": { "enabled": true, "title": "test_1-obj_1" @@ -25,8 +24,7 @@ "index": ".kibana", "source": { "coreMigrationVersion": "7.14.0", - "references": [ - ], + "references": [], "test-export-transform": { "enabled": true, "title": "test_1-obj_2" @@ -45,8 +43,7 @@ "index": ".kibana", "source": { "coreMigrationVersion": "7.14.0", - "references": [ - ], + "references": [], "test-export-add": { "title": "test_2-obj_1" }, @@ -64,8 +61,7 @@ "index": ".kibana", "source": { "coreMigrationVersion": "7.14.0", - "references": [ - ], + "references": [], "test-export-add": { "title": "test_2-obj_2" }, @@ -129,8 +125,7 @@ "index": ".kibana", "source": { "coreMigrationVersion": "7.14.0", - "references": [ - ], + "references": [], "test-export-invalid-transform": { "title": "test_2-obj_1" }, @@ -148,8 +143,7 @@ "index": ".kibana", "source": { "coreMigrationVersion": "7.14.0", - "references": [ - ], + "references": [], "test-export-transform-error": { "title": "test_2-obj_1" }, @@ -158,4 +152,5 @@ }, "type": "_doc" } -} \ No newline at end of file +} + diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_from_http_apis/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_from_http_apis/data.json index 8a6367d8f765..f53215f555ab 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_from_http_apis/data.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_from_http_apis/data.json @@ -25,3 +25,4 @@ } } } + diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_from_http_apis/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_from_http_apis/mappings.json deleted file mode 100644 index 3614080e5f58..000000000000 --- a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_from_http_apis/mappings.json +++ /dev/null @@ -1,3886 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": { - "is_hidden": true - }, - ".kibana_8.7.0": { - "is_hidden": true - } - }, - "index": ".kibana_1", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "38c91d092e0790ecd9ebe58e82145280", - "action_task_params": "d43ed71d8608a6406977fe093a9fbe74", - "alert": "bc1c55763564c3ebb167da6d324a0a05", - "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", - "apm-indices": "3d1b76c39bfb2cc8296b024d73854724", - "apm-server-schema": "b1d71908f324c17bf744ac72af5038fb", - "apm-service-group": "2af509c6506f29a858e5a0950577d9fa", - "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", - "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", - "cases": "fa60347cb6869de399b5b0e6ef49538f", - "cases-comments": "2ef945f765cefb6421548c1dc227ab49", - "cases-configure": "f32095d284444601d7b39a314751832c", - "cases-connector-mappings": "17d2e9e0e170a21a471285a5d845353c", - "cases-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "cases-user-actions": "4d3f0799ea1a3c48e0015da327e46848", - "config": "c63748b75f39d0c54de12d12c1ccbc20", - "config-global": "c63748b75f39d0c54de12d12c1ccbc20", - "connector_token": "53e34e65945ca21461be85f868495da5", - "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", - "created_at": "00da57df13e94e9d98437d13ace4bfe0", - "csp-rule-template": "8b03466c92eddc1458758ca6827eb540", - "csp_rule": "e962ddf765c0380b0fd57711e4684d4e", - "dashboard": "bc881edf161013a17165a244e0812e9f", - "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", - "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862", - "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "epm-packages": "77c148aa49f233d2a6554f513862bd38", - "epm-packages-assets": "44621b2f6052ef966da47b7c3a00f33b", - "event_loop_delays_daily": "5df7e292ddd5028e07c1482e130e6654", - "exception-list": "baf108c9934dda844921f692a513adae", - "exception-list-agnostic": "baf108c9934dda844921f692a513adae", - "file": "c38faa34f21af9cef4301a687de5dce0", - "file-upload-usage-collection-telemetry": "a34fbb8e3263d105044869264860c697", - "fileShare": "aa8f7ac2ddf8ab1a91bd34e347046caa", - "fleet-fleet-server-host": "434168167d1a1fd82fe92584ab9845c2", - "fleet-preconfiguration-deletion-record": "4c36f199189a367e43541f236141204c", - "fleet-proxy": "05b7a22977de25ce67a77e44dd8e6c33", - "graph-workspace": "27a94b2edcb0610c6aea54a7c56d7752", - "guided-onboarding-guide-state": "a3db59c45a3fd2730816d4f53c35c7d9", - "guided-onboarding-plugin-state": "3d1b76c39bfb2cc8296b024d73854724", - "index-pattern": "83c02d842fe2a94d14dfa13f7dcd6e87", - "infrastructure-monitoring-log-view": "c50526fc6040c5355ed027d34d05b35c", - "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", - "ingest-agent-policies": "6892b04ed54b03bceb7b99d204e9f719", - "ingest-download-sources": "5d602f6594f3b589dc9a8e3a44031522", - "ingest-outputs": "e2347e19e5c2a7448529d77abbf27f30", - "ingest-package-policies": "c9f0cc1a2ed81041e29b69f1f1f79b68", - "ingest_manager_settings": "a9f356eec1c40d030f433bbf47c2b479", - "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "legacy-url-alias": "0750774cf16475f88f2361e99cc5c8f0", - "lens": "52346cfec69ff7b47d5f0c12361a2797", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "9134b47593116d7953f6adba096fc463", - "maps-telemetry": "5ef305b18111b77789afefbd36b66171", - "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-job": "3bb64c31915acf93fc724af137a0891b", - "ml-module": "46ef4f0d6682636f0fff9799d6a2d7ac", - "ml-trained-model": "d2f03c1a5dd038fa58af14a56944312b", - "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", - "namespace": "2f4316de49999235636386fe51dc06c1", - "namespaces": "2f4316de49999235636386fe51dc06c1", - "originId": "2f4316de49999235636386fe51dc06c1", - "osquery-manager-usage-metric": "4dc4f647d27247c002f56f22742175fe", - "osquery-pack": "37af487fae033ccd92295fd20d1952f0", - "osquery-pack-asset": "989d30eeccb25d62980739dc7790a875", - "osquery-saved-query": "6bf77cfe41a55adb3a00d58e96d72ce9", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "f8d7012ea128c1f7eee2524e4ea346a6", - "search-session": "daa126fce3214044789ffe971a365145", - "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "security-rule": "8ae39a88fc70af3375b7050e8d8d5cc7", - "security-solution-signals-migration": "72761fd374ca11122ac8025a92b84fca", - "siem-detection-engine-rule-actions": "ec8a5cc503fc8cd151a2463190e4e425", - "siem-detection-engine-rule-execution-info": "f28f675a06fe27d22dc3be04005ac987", - "siem-ui-timeline": "0f4cc81427182c41cebd7d9c640ec253", - "siem-ui-timeline-note": "28393dfdeb4e4413393eb5f7ec8c5436", - "siem-ui-timeline-pinned-event": "293fce142548281599060e07ad2c9ddb", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "spaces-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "synthetics-monitor": "b7d62441939ad093caa011c7082a2af4", - "synthetics-param": "3d1b76c39bfb2cc8296b024d73854724", - "synthetics-privates-locations": "3d1b76c39bfb2cc8296b024d73854724", - "tag": "83d55da58f6530f7055415717ec06474", - "telemetry": "36a616f7026dfa617d6655df850fe16d", - "test-actions-export-hidden": "8b555f1f2684de2490682d4ac5dfbd2e", - "test-deprecations-plugin": "8e3906e5ac842a30d0de07848b59f6cb", - "test-export-add": "8e3906e5ac842a30d0de07848b59f6cb", - "test-export-add-dep": "8e3906e5ac842a30d0de07848b59f6cb", - "test-export-invalid-transform": "8e3906e5ac842a30d0de07848b59f6cb", - "test-export-transform": "8b555f1f2684de2490682d4ac5dfbd2e", - "test-export-transform-error": "8e3906e5ac842a30d0de07848b59f6cb", - "test-hidden-from-http-apis-importable-exportable": "8e3906e5ac842a30d0de07848b59f6cb", - "test-hidden-importable-exportable": "8e3906e5ac842a30d0de07848b59f6cb", - "test-hidden-non-importable-exportable": "8e3906e5ac842a30d0de07848b59f6cb", - "test-is-exportable": "8b555f1f2684de2490682d4ac5dfbd2e", - "test-not-hidden-from-http-apis-importable-exportable": "8e3906e5ac842a30d0de07848b59f6cb", - "test-not-visible-in-management": "8b555f1f2684de2490682d4ac5dfbd2e", - "test-visible-in-management": "8b555f1f2684de2490682d4ac5dfbd2e", - "test-with-display-name": "8b555f1f2684de2490682d4ac5dfbd2e", - "test_import_warning_1": "8e3906e5ac842a30d0de07848b59f6cb", - "test_import_warning_2": "8e3906e5ac842a30d0de07848b59f6cb", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-ml-upgrade-operation": "ece1011519ca8fd057364605744d75ac", - "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", - "upgrade-assistant-telemetry": "7443e25d76c53f49aaf2daac37d4d1f8", - "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", - "uptime-synthetics-api-key": "c3178f0fde61e18d3530ba9a70bc278a", - "url": "dde920c35ebae1bf43731d19f7b2194d", - "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", - "visualization": "f819cf6636b75c9e76ba733a0c6ef355", - "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" - } - }, - "dynamic": "strict", - "properties": { - "action": { - "properties": { - "actionTypeId": { - "type": "keyword" - }, - "config": { - "enabled": false, - "type": "object" - }, - "isMissingSecrets": { - "type": "boolean" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "secrets": { - "type": "binary" - } - } - }, - "action_task_params": { - "properties": { - "actionId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "consumer": { - "type": "keyword" - }, - "executionId": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - }, - "relatedSavedObjects": { - "enabled": false, - "type": "object" - } - } - }, - "alert": { - "properties": { - "actions": { - "properties": { - "actionRef": { - "type": "keyword" - }, - "actionTypeId": { - "type": "keyword" - }, - "frequency": { - "properties": { - "notifyWhen": { - "index": false, - "type": "keyword" - }, - "summary": { - "index": false, - "type": "boolean" - }, - "throttle": { - "index": false, - "type": "keyword" - } - } - }, - "group": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - }, - "type": "nested" - }, - "alertTypeId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "apiKeyOwner": { - "type": "keyword" - }, - "consumer": { - "type": "keyword" - }, - "createdAt": { - "type": "date" - }, - "createdBy": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "executionStatus": { - "properties": { - "error": { - "properties": { - "message": { - "type": "keyword" - }, - "reason": { - "type": "keyword" - } - } - }, - "lastDuration": { - "type": "long" - }, - "lastExecutionDate": { - "type": "date" - }, - "numberOfTriggeredActions": { - "type": "long" - }, - "status": { - "type": "keyword" - }, - "warning": { - "properties": { - "message": { - "type": "keyword" - }, - "reason": { - "type": "keyword" - } - } - } - } - }, - "lastRun": { - "properties": { - "alertsCount": { - "properties": { - "active": { - "type": "float" - }, - "ignored": { - "type": "float" - }, - "new": { - "type": "float" - }, - "recovered": { - "type": "float" - } - } - }, - "outcome": { - "type": "keyword" - }, - "outcomeMsg": { - "type": "text" - }, - "outcomeOrder": { - "type": "float" - }, - "warning": { - "type": "text" - } - } - }, - "legacyId": { - "type": "keyword" - }, - "mapped_params": { - "properties": { - "risk_score": { - "type": "float" - }, - "severity": { - "type": "keyword" - } - } - }, - "meta": { - "properties": { - "versionApiKeyLastmodified": { - "type": "keyword" - } - } - }, - "monitoring": { - "properties": { - "run": { - "properties": { - "calculated_metrics": { - "properties": { - "p50": { - "type": "long" - }, - "p95": { - "type": "long" - }, - "p99": { - "type": "long" - }, - "success_ratio": { - "type": "float" - } - } - }, - "history": { - "properties": { - "duration": { - "type": "long" - }, - "outcome": { - "type": "keyword" - }, - "success": { - "type": "boolean" - }, - "timestamp": { - "type": "date" - } - } - }, - "last_run": { - "properties": { - "metrics": { - "properties": { - "duration": { - "type": "long" - }, - "gap_duration_s": { - "type": "float" - }, - "total_alerts_created": { - "type": "float" - }, - "total_alerts_detected": { - "type": "float" - }, - "total_indexing_duration_ms": { - "type": "long" - }, - "total_search_duration_ms": { - "type": "long" - } - } - }, - "timestamp": { - "type": "date" - } - } - } - } - } - } - }, - "muteAll": { - "type": "boolean" - }, - "mutedInstanceIds": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "normalizer": "lowercase", - "type": "keyword" - } - }, - "type": "text" - }, - "nextRun": { - "type": "date" - }, - "notifyWhen": { - "type": "keyword" - }, - "params": { - "ignore_above": 4096, - "type": "flattened" - }, - "running": { - "type": "boolean" - }, - "schedule": { - "properties": { - "interval": { - "type": "keyword" - } - } - }, - "scheduledTaskId": { - "type": "keyword" - }, - "snoozeSchedule": { - "properties": { - "duration": { - "type": "long" - }, - "id": { - "type": "keyword" - }, - "rRule": { - "properties": { - "byhour": { - "type": "long" - }, - "byminute": { - "type": "long" - }, - "bymonth": { - "type": "short" - }, - "bymonthday": { - "type": "short" - }, - "bysecond": { - "type": "long" - }, - "bysetpos": { - "type": "long" - }, - "byweekday": { - "type": "keyword" - }, - "byweekno": { - "type": "short" - }, - "byyearday": { - "type": "short" - }, - "count": { - "type": "long" - }, - "dtstart": { - "format": "strict_date_time", - "type": "date" - }, - "freq": { - "type": "keyword" - }, - "interval": { - "type": "long" - }, - "tzid": { - "type": "keyword" - }, - "until": { - "format": "strict_date_time", - "type": "date" - }, - "wkst": { - "type": "keyword" - } - }, - "type": "nested" - }, - "skipRecurrences": { - "format": "strict_date_time", - "type": "date" - } - }, - "type": "nested" - }, - "tags": { - "type": "keyword" - }, - "throttle": { - "type": "keyword" - }, - "updatedAt": { - "type": "date" - }, - "updatedBy": { - "type": "keyword" - } - } - }, - "api_key_pending_invalidation": { - "properties": { - "apiKeyId": { - "type": "keyword" - }, - "createdAt": { - "type": "date" - } - } - }, - "apm-indices": { - "dynamic": "false", - "type": "object" - }, - "apm-server-schema": { - "properties": { - "schemaJson": { - "index": false, - "type": "text" - } - } - }, - "apm-service-group": { - "properties": { - "color": { - "type": "text" - }, - "description": { - "type": "text" - }, - "groupName": { - "type": "keyword" - }, - "kuery": { - "type": "text" - } - } - }, - "apm-telemetry": { - "dynamic": "false", - "type": "object" - }, - "app_search_telemetry": { - "dynamic": "false", - "type": "object" - }, - "application_usage_daily": { - "dynamic": "false", - "properties": { - "timestamp": { - "type": "date" - } - } - }, - "application_usage_totals": { - "dynamic": "false", - "type": "object" - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "content": { - "type": "text" - }, - "help": { - "type": "text" - }, - "image": { - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad-template": { - "dynamic": "false", - "properties": { - "help": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "tags": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "template_key": { - "type": "keyword" - } - } - }, - "cases": { - "properties": { - "assignees": { - "properties": { - "uid": { - "type": "keyword" - } - } - }, - "closed_at": { - "type": "date" - }, - "closed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "profile_uid": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "connector": { - "properties": { - "fields": { - "properties": { - "key": { - "type": "text" - }, - "value": { - "type": "text" - } - } - }, - "name": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "profile_uid": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "description": { - "type": "text" - }, - "duration": { - "type": "unsigned_long" - }, - "external_service": { - "properties": { - "connector_name": { - "type": "keyword" - }, - "external_id": { - "type": "keyword" - }, - "external_title": { - "type": "text" - }, - "external_url": { - "type": "text" - }, - "pushed_at": { - "type": "date" - }, - "pushed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "profile_uid": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "owner": { - "type": "keyword" - }, - "settings": { - "properties": { - "syncAlerts": { - "type": "boolean" - } - } - }, - "severity": { - "type": "short" - }, - "status": { - "type": "short" - }, - "tags": { - "type": "keyword" - }, - "title": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "total_alerts": { - "type": "integer" - }, - "total_comments": { - "type": "integer" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "profile_uid": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-comments": { - "properties": { - "actions": { - "properties": { - "targets": { - "properties": { - "endpointId": { - "type": "keyword" - }, - "hostname": { - "type": "keyword" - } - }, - "type": "nested" - }, - "type": { - "type": "keyword" - } - } - }, - "alertId": { - "type": "keyword" - }, - "comment": { - "type": "text" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "profile_uid": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "externalReferenceAttachmentTypeId": { - "type": "keyword" - }, - "externalReferenceId": { - "type": "keyword" - }, - "externalReferenceMetadata": { - "dynamic": "false", - "enabled": false, - "type": "object" - }, - "externalReferenceStorage": { - "dynamic": "false", - "properties": { - "type": { - "type": "keyword" - } - } - }, - "index": { - "type": "keyword" - }, - "owner": { - "type": "keyword" - }, - "persistableStateAttachmentState": { - "dynamic": "false", - "enabled": false, - "type": "object" - }, - "persistableStateAttachmentTypeId": { - "type": "keyword" - }, - "pushed_at": { - "type": "date" - }, - "pushed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "profile_uid": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "rule": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "profile_uid": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-configure": { - "properties": { - "closure_type": { - "type": "keyword" - }, - "connector": { - "properties": { - "fields": { - "properties": { - "key": { - "type": "text" - }, - "value": { - "type": "text" - } - } - }, - "name": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "profile_uid": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "owner": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "profile_uid": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-connector-mappings": { - "properties": { - "mappings": { - "properties": { - "action_type": { - "type": "keyword" - }, - "source": { - "type": "keyword" - }, - "target": { - "type": "keyword" - } - } - }, - "owner": { - "type": "keyword" - } - } - }, - "cases-telemetry": { - "dynamic": "false", - "type": "object" - }, - "cases-user-actions": { - "properties": { - "action": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "profile_uid": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "owner": { - "type": "keyword" - }, - "payload": { - "dynamic": "false", - "properties": { - "connector": { - "properties": { - "type": { - "type": "keyword" - } - } - } - } - }, - "type": { - "type": "keyword" - } - } - }, - "config": { - "dynamic": "false", - "properties": { - "buildNum": { - "type": "keyword" - } - } - }, - "config-global": { - "dynamic": "false", - "properties": { - "buildNum": { - "type": "keyword" - } - } - }, - "connector_token": { - "properties": { - "connectorId": { - "type": "keyword" - }, - "createdAt": { - "type": "date" - }, - "expiresAt": { - "type": "date" - }, - "token": { - "type": "binary" - }, - "tokenType": { - "type": "keyword" - }, - "updatedAt": { - "type": "date" - } - } - }, - "core-usage-stats": { - "dynamic": "false", - "type": "object" - }, - "coreMigrationVersion": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "csp-rule-template": { - "dynamic": "false", - "properties": { - "metadata": { - "properties": { - "benchmark": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - } - } - }, - "csp_rule": { - "dynamic": "false", - "properties": { - "enabled": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "boolean" - }, - "metadata": { - "properties": { - "name": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "package_policy_id": { - "type": "keyword" - }, - "policy_id": { - "type": "keyword" - } - } - }, - "dashboard": { - "properties": { - "controlGroupInput": { - "properties": { - "chainingSystem": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "controlStyle": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "ignoreParentSettingsJSON": { - "index": false, - "type": "text" - }, - "panelsJSON": { - "index": false, - "type": "text" - } - } - }, - "description": { - "type": "text" - }, - "hits": { - "doc_values": false, - "index": false, - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "index": false, - "type": "text" - } - } - }, - "optionsJSON": { - "index": false, - "type": "text" - }, - "panelsJSON": { - "index": false, - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "pause": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "section": { - "doc_values": false, - "index": false, - "type": "integer" - }, - "value": { - "doc_values": false, - "index": false, - "type": "integer" - } - } - }, - "timeFrom": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "timeRestore": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "timeTo": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "endpoint:user-artifact": { - "properties": { - "body": { - "type": "binary" - }, - "compressionAlgorithm": { - "index": false, - "type": "keyword" - }, - "created": { - "index": false, - "type": "date" - }, - "decodedSha256": { - "index": false, - "type": "keyword" - }, - "decodedSize": { - "index": false, - "type": "long" - }, - "encodedSha256": { - "type": "keyword" - }, - "encodedSize": { - "index": false, - "type": "long" - }, - "encryptionAlgorithm": { - "index": false, - "type": "keyword" - }, - "identifier": { - "type": "keyword" - } - } - }, - "endpoint:user-artifact-manifest": { - "properties": { - "artifacts": { - "properties": { - "artifactId": { - "index": false, - "type": "keyword" - }, - "policyId": { - "index": false, - "type": "keyword" - } - }, - "type": "nested" - }, - "created": { - "index": false, - "type": "date" - }, - "schemaVersion": { - "type": "keyword" - }, - "semanticVersion": { - "index": false, - "type": "keyword" - } - } - }, - "enterprise_search_telemetry": { - "dynamic": "false", - "type": "object" - }, - "epm-packages": { - "properties": { - "es_index_patterns": { - "enabled": false, - "type": "object" - }, - "experimental_data_stream_features": { - "properties": { - "data_stream": { - "type": "keyword" - }, - "features": { - "properties": { - "synthetic_source": { - "type": "boolean" - }, - "tsdb": { - "type": "boolean" - } - }, - "type": "nested" - } - }, - "type": "nested" - }, - "install_format_schema_version": { - "type": "version" - }, - "install_source": { - "type": "keyword" - }, - "install_started_at": { - "type": "date" - }, - "install_status": { - "type": "keyword" - }, - "install_version": { - "type": "keyword" - }, - "installed_es": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - }, - "type": "nested" - }, - "installed_kibana": { - "enabled": false, - "type": "object" - }, - "installed_kibana_space_id": { - "type": "keyword" - }, - "internal": { - "type": "boolean" - }, - "keep_policies_up_to_date": { - "index": false, - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "package_assets": { - "enabled": false, - "type": "object" - }, - "verification_key_id": { - "type": "keyword" - }, - "verification_status": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "epm-packages-assets": { - "properties": { - "asset_path": { - "type": "keyword" - }, - "data_base64": { - "type": "binary" - }, - "data_utf8": { - "index": false, - "type": "text" - }, - "install_source": { - "type": "keyword" - }, - "media_type": { - "type": "keyword" - }, - "package_name": { - "type": "keyword" - }, - "package_version": { - "type": "keyword" - } - } - }, - "event_loop_delays_daily": { - "dynamic": "false", - "properties": { - "lastUpdatedAt": { - "type": "date" - } - } - }, - "exception-list": { - "properties": { - "_tags": { - "type": "keyword" - }, - "comments": { - "properties": { - "comment": { - "type": "keyword" - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "updated_at": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "description": { - "type": "keyword" - }, - "entries": { - "properties": { - "entries": { - "properties": { - "field": { - "type": "keyword" - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "field": { - "type": "keyword" - }, - "list": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "immutable": { - "type": "boolean" - }, - "item_id": { - "type": "keyword" - }, - "list_id": { - "type": "keyword" - }, - "list_type": { - "type": "keyword" - }, - "meta": { - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - }, - "os_types": { - "type": "keyword" - }, - "tags": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - }, - "tie_breaker_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "exception-list-agnostic": { - "properties": { - "_tags": { - "type": "keyword" - }, - "comments": { - "properties": { - "comment": { - "type": "keyword" - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "updated_at": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "description": { - "type": "keyword" - }, - "entries": { - "properties": { - "entries": { - "properties": { - "field": { - "type": "keyword" - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "field": { - "type": "keyword" - }, - "list": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "immutable": { - "type": "boolean" - }, - "item_id": { - "type": "keyword" - }, - "list_id": { - "type": "keyword" - }, - "list_type": { - "type": "keyword" - }, - "meta": { - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - }, - "os_types": { - "type": "keyword" - }, - "tags": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - }, - "tie_breaker_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "file": { - "dynamic": "false", - "properties": { - "FileKind": { - "type": "keyword" - }, - "Meta": { - "type": "flattened" - }, - "Status": { - "type": "keyword" - }, - "Updated": { - "type": "date" - }, - "created": { - "type": "date" - }, - "extension": { - "type": "keyword" - }, - "mime_type": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "size": { - "type": "long" - }, - "user": { - "type": "flattened" - } - } - }, - "file-upload-usage-collection-telemetry": { - "properties": { - "file_upload": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "fileShare": { - "dynamic": "false", - "properties": { - "created": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "token": { - "type": "keyword" - }, - "valid_until": { - "type": "long" - } - } - }, - "fleet-fleet-server-host": { - "properties": { - "host_urls": { - "index": false, - "type": "keyword" - }, - "is_default": { - "type": "boolean" - }, - "is_preconfigured": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "proxy_id": { - "type": "keyword" - } - } - }, - "fleet-preconfiguration-deletion-record": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "fleet-proxy": { - "properties": { - "certificate": { - "index": false, - "type": "keyword" - }, - "certificate_authorities": { - "index": false, - "type": "keyword" - }, - "certificate_key": { - "index": false, - "type": "keyword" - }, - "is_preconfigured": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "proxy_headers": { - "index": false, - "type": "text" - }, - "url": { - "index": false, - "type": "keyword" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "legacyIndexPatternRef": { - "index": false, - "type": "text" - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "guided-onboarding-guide-state": { - "dynamic": "false", - "properties": { - "guideId": { - "type": "keyword" - }, - "isActive": { - "type": "boolean" - } - } - }, - "guided-onboarding-plugin-state": { - "dynamic": "false", - "type": "object" - }, - "index-pattern": { - "dynamic": "false", - "properties": { - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "infrastructure-monitoring-log-view": { - "dynamic": "false", - "properties": { - "name": { - "type": "text" - } - } - }, - "infrastructure-ui-source": { - "dynamic": "false", - "type": "object" - }, - "ingest-agent-policies": { - "properties": { - "data_output_id": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "download_source_id": { - "type": "keyword" - }, - "fleet_server_host_id": { - "type": "keyword" - }, - "inactivity_timeout": { - "type": "integer" - }, - "is_default": { - "type": "boolean" - }, - "is_default_fleet_server": { - "type": "boolean" - }, - "is_managed": { - "type": "boolean" - }, - "is_preconfigured": { - "type": "keyword" - }, - "monitoring_enabled": { - "index": false, - "type": "keyword" - }, - "monitoring_output_id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "namespace": { - "type": "keyword" - }, - "revision": { - "type": "integer" - }, - "schema_version": { - "type": "version" - }, - "status": { - "type": "keyword" - }, - "unenroll_timeout": { - "type": "integer" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "ingest-download-sources": { - "properties": { - "host": { - "type": "keyword" - }, - "is_default": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "source_id": { - "index": false, - "type": "keyword" - } - } - }, - "ingest-outputs": { - "properties": { - "ca_sha256": { - "index": false, - "type": "keyword" - }, - "ca_trusted_fingerprint": { - "index": false, - "type": "keyword" - }, - "config": { - "type": "flattened" - }, - "config_yaml": { - "type": "text" - }, - "hosts": { - "type": "keyword" - }, - "is_default": { - "type": "boolean" - }, - "is_default_monitoring": { - "type": "boolean" - }, - "is_preconfigured": { - "index": false, - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "output_id": { - "index": false, - "type": "keyword" - }, - "proxy_id": { - "type": "keyword" - }, - "shipper": { - "dynamic": "false", - "type": "object" - }, - "ssl": { - "type": "binary" - }, - "type": { - "type": "keyword" - } - } - }, - "ingest-package-policies": { - "properties": { - "created_at": { - "type": "date" - }, - "created_by": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "elasticsearch": { - "enabled": false, - "properties": { - "privileges": { - "properties": { - "cluster": { - "type": "keyword" - } - } - } - } - }, - "enabled": { - "type": "boolean" - }, - "inputs": { - "enabled": false, - "properties": { - "compiled_input": { - "type": "flattened" - }, - "config": { - "type": "flattened" - }, - "enabled": { - "type": "boolean" - }, - "policy_template": { - "type": "keyword" - }, - "streams": { - "properties": { - "compiled_stream": { - "type": "flattened" - }, - "config": { - "type": "flattened" - }, - "data_stream": { - "properties": { - "dataset": { - "type": "keyword" - }, - "elasticsearch": { - "properties": { - "privileges": { - "type": "flattened" - } - } - }, - "type": { - "type": "keyword" - } - } - }, - "enabled": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "vars": { - "type": "flattened" - } - }, - "type": "nested" - }, - "type": { - "type": "keyword" - }, - "vars": { - "type": "flattened" - } - }, - "type": "nested" - }, - "is_managed": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "namespace": { - "type": "keyword" - }, - "package": { - "properties": { - "name": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "policy_id": { - "type": "keyword" - }, - "revision": { - "type": "integer" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "type": "keyword" - }, - "vars": { - "type": "flattened" - } - } - }, - "ingest_manager_settings": { - "properties": { - "fleet_server_hosts": { - "type": "keyword" - }, - "has_seen_add_data_notice": { - "index": false, - "type": "boolean" - }, - "prerelease_integrations_enabled": { - "type": "boolean" - } - } - }, - "inventory-view": { - "dynamic": "false", - "type": "object" - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "legacy-url-alias": { - "dynamic": "false", - "properties": { - "disabled": { - "type": "boolean" - }, - "resolveCounter": { - "type": "long" - }, - "sourceId": { - "type": "keyword" - }, - "targetId": { - "type": "keyword" - }, - "targetNamespace": { - "type": "keyword" - }, - "targetType": { - "type": "keyword" - } - } - }, - "lens": { - "properties": { - "description": { - "type": "text" - }, - "expression": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "state": { - "type": "flattened" - }, - "title": { - "type": "text" - }, - "visualizationType": { - "type": "keyword" - } - } - }, - "lens-ui-telemetry": { - "properties": { - "count": { - "type": "integer" - }, - "date": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "map": { - "properties": { - "bounds": { - "dynamic": "false", - "type": "object" - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "enabled": false, - "type": "object" - }, - "metrics-explorer-view": { - "dynamic": "false", - "type": "object" - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "canvas-workpad-template": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "config": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "ml-job": { - "properties": { - "datafeed_id": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "job_id": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "ml-module": { - "dynamic": "false", - "properties": { - "datafeeds": { - "type": "object" - }, - "defaultIndexPattern": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "description": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "id": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "jobs": { - "type": "object" - }, - "logo": { - "type": "object" - }, - "query": { - "type": "object" - }, - "title": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "type": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "ml-trained-model": { - "properties": { - "job": { - "properties": { - "create_time": { - "type": "date" - }, - "job_id": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "model_id": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "monitoring-telemetry": { - "properties": { - "reportedClusterUuids": { - "type": "keyword" - } - } - }, - "namespace": { - "type": "keyword" - }, - "namespaces": { - "type": "keyword" - }, - "originId": { - "type": "keyword" - }, - "osquery-manager-usage-metric": { - "properties": { - "count": { - "type": "long" - }, - "errors": { - "type": "long" - } - } - }, - "osquery-pack": { - "properties": { - "created_at": { - "type": "date" - }, - "created_by": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "enabled": { - "type": "boolean" - }, - "name": { - "type": "text" - }, - "queries": { - "dynamic": "false", - "properties": { - "ecs_mapping": { - "enabled": false, - "type": "object" - }, - "id": { - "type": "keyword" - }, - "interval": { - "type": "text" - }, - "platform": { - "type": "keyword" - }, - "query": { - "type": "text" - }, - "version": { - "type": "keyword" - } - } - }, - "shards": { - "enabled": false, - "type": "object" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "osquery-pack-asset": { - "dynamic": "false", - "properties": { - "description": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queries": { - "dynamic": "false", - "properties": { - "ecs_mapping": { - "enabled": false, - "type": "object" - }, - "id": { - "type": "keyword" - }, - "interval": { - "type": "text" - }, - "platform": { - "type": "keyword" - }, - "query": { - "type": "text" - }, - "version": { - "type": "keyword" - } - } - }, - "shards": { - "enabled": false, - "type": "object" - }, - "version": { - "type": "long" - } - } - }, - "osquery-saved-query": { - "dynamic": "false", - "properties": { - "created_at": { - "type": "date" - }, - "created_by": { - "type": "text" - }, - "description": { - "type": "text" - }, - "ecs_mapping": { - "enabled": false, - "type": "object" - }, - "id": { - "type": "keyword" - }, - "interval": { - "type": "keyword" - }, - "platform": { - "type": "keyword" - }, - "query": { - "type": "text" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "type": "text" - }, - "version": { - "type": "keyword" - } - } - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "index": false, - "type": "keyword" - } - } - }, - "timefilter": { - "enabled": false, - "type": "object" - }, - "title": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "breakdownField": { - "type": "text" - }, - "columns": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "description": { - "type": "text" - }, - "grid": { - "enabled": false, - "type": "object" - }, - "hideAggregatedPreview": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "hideChart": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "hits": { - "doc_values": false, - "index": false, - "type": "integer" - }, - "isTextBasedQuery": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "index": false, - "type": "text" - } - } - }, - "refreshInterval": { - "dynamic": "false", - "properties": { - "pause": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "value": { - "doc_values": false, - "index": false, - "type": "integer" - } - } - }, - "rowHeight": { - "type": "text" - }, - "rowsPerPage": { - "doc_values": false, - "index": false, - "type": "integer" - }, - "sort": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "timeRange": { - "dynamic": "false", - "properties": { - "from": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "to": { - "doc_values": false, - "index": false, - "type": "keyword" - } - } - }, - "timeRestore": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "title": { - "type": "text" - }, - "usesAdHocDataView": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "version": { - "type": "integer" - }, - "viewMode": { - "doc_values": false, - "index": false, - "type": "keyword" - } - } - }, - "search-session": { - "properties": { - "appId": { - "type": "keyword" - }, - "created": { - "type": "date" - }, - "expires": { - "type": "date" - }, - "idMapping": { - "enabled": false, - "type": "object" - }, - "initialState": { - "enabled": false, - "type": "object" - }, - "isCanceled": { - "type": "boolean" - }, - "locatorId": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "realmName": { - "type": "keyword" - }, - "realmType": { - "type": "keyword" - }, - "restoreState": { - "enabled": false, - "type": "object" - }, - "sessionId": { - "type": "keyword" - }, - "username": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "search-telemetry": { - "dynamic": "false", - "type": "object" - }, - "security-rule": { - "dynamic": "false", - "properties": { - "name": { - "type": "keyword" - }, - "rule_id": { - "type": "keyword" - }, - "version": { - "type": "long" - } - } - }, - "security-solution-signals-migration": { - "properties": { - "created": { - "index": false, - "type": "date" - }, - "createdBy": { - "index": false, - "type": "text" - }, - "destinationIndex": { - "index": false, - "type": "keyword" - }, - "error": { - "index": false, - "type": "text" - }, - "sourceIndex": { - "type": "keyword" - }, - "status": { - "index": false, - "type": "keyword" - }, - "taskId": { - "index": false, - "type": "keyword" - }, - "updated": { - "index": false, - "type": "date" - }, - "updatedBy": { - "index": false, - "type": "text" - }, - "version": { - "type": "long" - } - } - }, - "siem-detection-engine-rule-actions": { - "properties": { - "actions": { - "properties": { - "actionRef": { - "type": "keyword" - }, - "action_type_id": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - } - }, - "alertThrottle": { - "type": "keyword" - }, - "ruleAlertId": { - "type": "keyword" - }, - "ruleThrottle": { - "type": "keyword" - } - } - }, - "siem-detection-engine-rule-execution-info": { - "properties": { - "last_execution": { - "properties": { - "date": { - "type": "date" - }, - "message": { - "type": "text" - }, - "metrics": { - "properties": { - "execution_gap_duration_s": { - "type": "long" - }, - "total_enrichment_duration_ms": { - "type": "long" - }, - "total_indexing_duration_ms": { - "type": "long" - }, - "total_search_duration_ms": { - "type": "long" - } - } - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "status_order": { - "type": "long" - } - } - } - } - }, - "siem-ui-timeline": { - "properties": { - "columns": { - "properties": { - "aggregatable": { - "type": "boolean" - }, - "category": { - "type": "keyword" - }, - "columnHeaderType": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "example": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "indexes": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "placeholder": { - "type": "text" - }, - "searchable": { - "type": "boolean" - }, - "type": { - "type": "keyword" - } - } - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "dataProviders": { - "properties": { - "and": { - "properties": { - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - }, - "type": { - "type": "text" - } - } - }, - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - }, - "type": { - "type": "text" - } - } - }, - "dateRange": { - "properties": { - "end": { - "type": "date" - }, - "start": { - "type": "date" - } - } - }, - "description": { - "type": "text" - }, - "eqlOptions": { - "properties": { - "eventCategoryField": { - "type": "text" - }, - "query": { - "type": "text" - }, - "size": { - "type": "text" - }, - "tiebreakerField": { - "type": "text" - }, - "timestampField": { - "type": "text" - } - } - }, - "eventType": { - "type": "keyword" - }, - "excludedRowRendererIds": { - "type": "text" - }, - "favorite": { - "properties": { - "favoriteDate": { - "type": "date" - }, - "fullName": { - "type": "text" - }, - "keySearch": { - "type": "text" - }, - "userName": { - "type": "text" - } - } - }, - "filters": { - "properties": { - "exists": { - "type": "text" - }, - "match_all": { - "type": "text" - }, - "meta": { - "properties": { - "alias": { - "type": "text" - }, - "controlledBy": { - "type": "text" - }, - "disabled": { - "type": "boolean" - }, - "field": { - "type": "text" - }, - "formattedValue": { - "type": "text" - }, - "index": { - "type": "keyword" - }, - "key": { - "type": "keyword" - }, - "negate": { - "type": "boolean" - }, - "params": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "text" - } - } - }, - "missing": { - "type": "text" - }, - "query": { - "type": "text" - }, - "range": { - "type": "text" - }, - "script": { - "type": "text" - } - } - }, - "indexNames": { - "type": "text" - }, - "kqlMode": { - "type": "keyword" - }, - "kqlQuery": { - "properties": { - "filterQuery": { - "properties": { - "kuery": { - "properties": { - "expression": { - "type": "text" - }, - "kind": { - "type": "keyword" - } - } - }, - "serializedQuery": { - "type": "text" - } - } - } - } - }, - "sort": { - "dynamic": "false", - "properties": { - "columnId": { - "type": "keyword" - }, - "columnType": { - "type": "keyword" - }, - "sortDirection": { - "type": "keyword" - } - } - }, - "status": { - "type": "keyword" - }, - "templateTimelineId": { - "type": "text" - }, - "templateTimelineVersion": { - "type": "integer" - }, - "timelineType": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-note": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "note": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-pinned-event": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "imageUrl": { - "index": false, - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "spaces-usage-stats": { - "dynamic": "false", - "type": "object" - }, - "synthetics-monitor": { - "dynamic": "false", - "properties": { - "custom_heartbeat_id": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "hash": { - "type": "keyword" - }, - "hosts": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "id": { - "type": "keyword" - }, - "journey_id": { - "type": "keyword" - }, - "locations": { - "properties": { - "id": { - "fields": { - "text": { - "type": "text" - } - }, - "ignore_above": 256, - "type": "keyword" - }, - "label": { - "type": "text" - } - } - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 256, - "normalizer": "lowercase", - "type": "keyword" - } - }, - "type": "text" - }, - "origin": { - "type": "keyword" - }, - "project_id": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - }, - "schedule": { - "properties": { - "number": { - "type": "integer" - } - } - }, - "tags": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - }, - "type": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "urls": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "synthetics-param": { - "dynamic": "false", - "type": "object" - }, - "synthetics-privates-locations": { - "dynamic": "false", - "type": "object" - }, - "tag": { - "properties": { - "color": { - "type": "text" - }, - "description": { - "type": "text" - }, - "name": { - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "allowChangingOptInStatus": { - "type": "boolean" - }, - "enabled": { - "type": "boolean" - }, - "lastReported": { - "type": "date" - }, - "lastVersionChecked": { - "type": "keyword" - }, - "reportFailureCount": { - "type": "integer" - }, - "reportFailureVersion": { - "type": "keyword" - }, - "sendUsageFrom": { - "type": "keyword" - }, - "userHasSeenNotice": { - "type": "boolean" - } - } - }, - "test-actions-export-hidden": { - "properties": { - "enabled": { - "type": "boolean" - }, - "title": { - "type": "text" - } - } - }, - "test-deprecations-plugin": { - "properties": { - "title": { - "type": "text" - } - } - }, - "test-export-add": { - "properties": { - "title": { - "type": "text" - } - } - }, - "test-export-add-dep": { - "properties": { - "title": { - "type": "text" - } - } - }, - "test-export-invalid-transform": { - "properties": { - "title": { - "type": "text" - } - } - }, - "test-export-transform": { - "properties": { - "enabled": { - "type": "boolean" - }, - "title": { - "type": "text" - } - } - }, - "test-export-transform-error": { - "properties": { - "title": { - "type": "text" - } - } - }, - "test-hidden-from-http-apis-importable-exportable": { - "properties": { - "title": { - "type": "text" - } - } - }, - "test-hidden-importable-exportable": { - "properties": { - "title": { - "type": "text" - } - } - }, - "test-hidden-non-importable-exportable": { - "properties": { - "title": { - "type": "text" - } - } - }, - "test-is-exportable": { - "properties": { - "enabled": { - "type": "boolean" - }, - "title": { - "type": "text" - } - } - }, - "test-not-hidden-from-http-apis-importable-exportable": { - "properties": { - "title": { - "type": "text" - } - } - }, - "test-not-visible-in-management": { - "properties": { - "enabled": { - "type": "boolean" - }, - "title": { - "type": "text" - } - } - }, - "test-visible-in-management": { - "properties": { - "enabled": { - "type": "boolean" - }, - "title": { - "type": "text" - } - } - }, - "test-with-display-name": { - "properties": { - "enabled": { - "type": "boolean" - }, - "title": { - "type": "text" - } - } - }, - "test_import_warning_1": { - "properties": { - "title": { - "type": "text" - } - } - }, - "test_import_warning_2": { - "properties": { - "title": { - "type": "text" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-ml-upgrade-operation": { - "properties": { - "jobId": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "nodeId": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "snapshotId": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "status": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "upgrade-assistant-reindex-operation": { - "properties": { - "errorMessage": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "indexName": { - "type": "keyword" - }, - "lastCompletedStep": { - "type": "long" - }, - "locked": { - "type": "date" - }, - "newIndexName": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "reindexOptions": { - "properties": { - "openAndClose": { - "type": "boolean" - }, - "queueSettings": { - "properties": { - "queuedAt": { - "type": "long" - }, - "startedAt": { - "type": "long" - } - } - } - } - }, - "reindexTaskId": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "reindexTaskPercComplete": { - "type": "float" - }, - "runningReindexCount": { - "type": "integer" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "null_value": true, - "type": "boolean" - } - } - } - } - } - } - }, - "uptime-dynamic-settings": { - "dynamic": "false", - "type": "object" - }, - "uptime-synthetics-api-key": { - "dynamic": "false", - "properties": { - "apiKey": { - "type": "binary" - } - } - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "locatorJSON": { - "index": false, - "type": "text" - }, - "slug": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "usage-counters": { - "dynamic": "false", - "properties": { - "domainId": { - "type": "keyword" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "index": false, - "type": "text" - } - } - }, - "savedSearchRefName": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "index": false, - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "index": false, - "type": "text" - } - } - }, - "workplace_search_telemetry": { - "dynamic": "false", - "type": "object" - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "hidden": "true", - "mapping": { - "total_fields": { - "limit": "1500" - } - }, - "number_of_replicas": "0", - "number_of_shards": "1", - "priority": "10", - "refresh_interval": "1s" - } - } - } -} diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/data.json index cc38a9affdda..065db0830fb3 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/data.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/data.json @@ -5,8 +5,7 @@ "index": ".kibana", "source": { "coreMigrationVersion": "7.14.0", - "references": [ - ], + "references": [], "test-hidden-non-importable-exportable": { "title": "Hidden Saved object type that is not importable/exportable." }, @@ -16,3 +15,4 @@ "type": "_doc" } } + diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/data.json index 057373579c10..3dfde2c24886 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/data.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/data.json @@ -35,8 +35,7 @@ { "type": "doc", "value": { - "index": ".kibana", - "type": "doc", + "index": ".kibana_analytics", "id": "visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed", "source": { "visualization": { @@ -50,13 +49,18 @@ } }, "type": "visualization", - "updated_at": "2019-01-22T19:32:31.206Z" + "updated_at": "2019-01-22T19:32:31.206Z", + "namespaces": [ + "default" + ], + "coreMigrationVersion": "8.8.0", + "typeMigrationVersion": "8.0.0" }, - "references" : [ + "references": [ { - "name" : "kibanaSavedObjectMeta.searchSourceJSON.index", - "type" : "index-pattern", - "id" : "logstash-*" + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern", + "id": "logstash-*" } ] } @@ -65,8 +69,7 @@ { "type": "doc", "value": { - "index": ".kibana", - "type": "doc", + "index": ".kibana_analytics", "id": "dashboard:i-exist", "source": { "dashboard": { @@ -82,7 +85,13 @@ } }, "type": "dashboard", - "updated_at": "2019-01-22T19:32:47.232Z" + "updated_at": "2019-01-22T19:32:47.232Z", + "namespaces": [ + "default" + ], + "coreMigrationVersion": "8.8.0", + "typeMigrationVersion": "8.0.0" } } } + diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/mappings.json deleted file mode 100644 index adcf4164668d..000000000000 --- a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/mappings.json +++ /dev/null @@ -1,466 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "settings": { - "index": { - "number_of_shards": "1", - "auto_expand_replicas": "0-1", - "number_of_replicas": "0" - } - }, - "mappings": { - "dynamic": true, - "properties": { - "test-actions-export-hidden": { - "properties": { - "title": { "type": "text" } - } - }, - "test-export-transform": { - "properties": { - "title": { "type": "text" }, - "enabled": { "type": "boolean" } - } - }, - "test-export-add": { - "properties": { - "title": { "type": "text" } - } - }, - "test-export-add-dep": { - "properties": { - "title": { "type": "text" } - } - }, - "test-export-transform-error": { - "properties": { - "title": { "type": "text" } - } - }, - "test-export-invalid-transform": { - "properties": { - "title": { "type": "text" } - } - }, - "apm-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "go": { - "type": "long", - "null_value": 0 - }, - "java": { - "type": "long", - "null_value": 0 - }, - "js-base": { - "type": "long", - "null_value": 0 - }, - "nodejs": { - "type": "long", - "null_value": 0 - }, - "python": { - "type": "long", - "null_value": 0 - }, - "ruby": { - "type": "long", - "null_value": 0 - } - } - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "id": { - "type": "text", - "index": false - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "accessibility:disableAnimations": { - "type": "boolean" - }, - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "defaultIndex": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "telemetry:optIn": { - "type": "boolean" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "map": { - "properties": { - "bounds": { - "dynamic": false, - "properties": {} - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "space": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "initials": { - "type": "keyword" - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "spaceId": { - "type": "keyword" - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - } - } - } - } -} diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform/data.json index 3c311b046519..88de6cf244be 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform/data.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/nested_export_transform/data.json @@ -35,8 +35,7 @@ "index": ".kibana", "source": { "coreMigrationVersion": "7.14.0", - "references": [ - ], + "references": [], "test-export-transform": { "enabled": true, "title": "test_1-obj_2" @@ -55,8 +54,7 @@ "index": ".kibana", "source": { "coreMigrationVersion": "7.14.0", - "references": [ - ], + "references": [], "test-export-add": { "title": "test_2-obj_1" }, @@ -88,4 +86,5 @@ }, "type": "_doc" } -} \ No newline at end of file +} + diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management/data.json index e277faf82792..f6099f319d3f 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management/data.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/visible_in_management/data.json @@ -5,8 +5,7 @@ "index": ".kibana", "source": { "coreMigrationVersion": "7.14.0", - "references": [ - ], + "references": [], "test-not-visible-in-management": { "enabled": true, "title": "vim-1" @@ -17,3 +16,4 @@ "type": "_doc" } } + diff --git a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap index 55bbf9d8b5fc..3c1f38e8a7a1 100644 --- a/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/apm_telemetry.test.ts.snap @@ -937,6 +937,16 @@ exports[`APM telemetry helpers getApmTelemetry generates a JSON object with the "type": "long" } } + }, + "environments": { + "properties": { + "1d": { + "type": "long", + "_meta": { + "description": "Total number of unique environments within the last day" + } + } + } } } }, diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts index 0c4f7b46e845..ce9133ebd8ff 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts @@ -776,28 +776,31 @@ export const tasks: TelemetryTask[] = [ }) ).hits.total.value; - const servicesCount = ( - await telemetryClient.search({ - index: [indices.transaction, indices.error, indices.metric], - body: { - track_total_hits: false, - size: 0, - timeout, - query: { - bool: { - filter: [range1d], + const servicesAndEnvironmentsCount = await telemetryClient.search({ + index: [indices.transaction, indices.error, indices.metric], + body: { + track_total_hits: false, + size: 0, + timeout, + query: { + bool: { + filter: [range1d], + }, + }, + aggs: { + service_name: { + cardinality: { + field: SERVICE_NAME, }, }, - aggs: { - service_name: { - cardinality: { - field: SERVICE_NAME, - }, + service_environments: { + cardinality: { + field: SERVICE_ENVIRONMENT, }, }, }, - }) - ).aggregations?.service_name.value; + }, + }); return { counts: { @@ -811,7 +814,14 @@ export const tasks: TelemetryTask[] = [ '1d': tracesPerDayCount || 0, }, services: { - '1d': servicesCount || 0, + '1d': + servicesAndEnvironmentsCount.aggregations?.service_name.value || + 0, + }, + environments: { + '1d': + servicesAndEnvironmentsCount.aggregations?.service_environments + .value || 0, }, }, }; diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts index 3dacba9c68a0..4b6e00c3a18a 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/schema.ts @@ -202,6 +202,15 @@ export const apmSchema: MakeSchemaFrom = { max_error_groups_per_service: timeframeMapSchema, traces: timeframeMapSchema, services: timeframeMapSchema, + environments: { + '1d': { + ...long, + _meta: { + description: + 'Total number of unique environments within the last day', + }, + }, + }, }, cardinality: { client: { geo: { country_iso_code: { rum: timeframeMap1dSchema } } }, diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts index f7f3f07fca45..ae4906feb7c7 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/types.ts @@ -108,6 +108,7 @@ export interface APMUsage { max_error_groups_per_service: TimeframeMap; traces: TimeframeMap; services: TimeframeMap; + environments: TimeframeMap1d; }; cardinality: { client: { geo: { country_iso_code: { rum: TimeframeMap1d } } }; diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts index 16128d130894..8c4bdfcb41c3 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -89,11 +89,7 @@ export const cloudPostureIntegrations: CloudPostureIntegrations = { benchmark: i18n.translate('xpack.csp.cspmIntegration.gcpOption.benchmarkTitle', { defaultMessage: 'CIS GCP', }), - disabled: true, icon: 'logoGCP', - tooltip: i18n.translate('xpack.csp.cspmIntegration.gcpOption.tooltipContent', { - defaultMessage: 'Coming soon', - }), }, { type: CLOUDBEAT_AZURE, @@ -214,3 +210,4 @@ export const cloudPostureIntegrations: CloudPostureIntegrations = { }, }; export const FINDINGS_DOCS_URL = 'https://ela.st/findings'; +export const MIN_VERSION_GCP_CIS = '1.5.0'; diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx new file mode 100644 index 000000000000..eeafb3cc40e0 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx @@ -0,0 +1,295 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { + EuiFieldText, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiText, + EuiTitle, + EuiSelect, + EuiForm, + EuiCallOut, + EuiTextArea, +} from '@elastic/eui'; +import type { NewPackagePolicy } from '@kbn/fleet-plugin/public'; +import { NewPackagePolicyInput, PackageInfo } from '@kbn/fleet-plugin/common'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { RadioGroup } from './csp_boxed_radio_group'; +import { getPosturePolicy, NewPackagePolicyPostureInput } from './utils'; +import { MIN_VERSION_GCP_CIS } from '../../common/constants'; + +type SetupFormatGCP = 'google_cloud_shell' | 'manual'; +const GCPSetupInfoContent = () => ( + <> + + +

+ +

+
+ + + + + +); + +/* NEED TO FIND THE REAL URL HERE LATER*/ +const DocsLink = ( + + + documentation + + ), + }} + /> + +); + +const CredentialFileText = i18n.translate( + 'xpack.csp.findings.gcpIntegration.gcpInputText.credentialFileText', + { defaultMessage: 'Path to JSON file containing the credentials and key used to subscribe' } +); +const CredentialJSONText = i18n.translate( + 'xpack.csp.findings.gcpIntegration.gcpInputText.credentialJSONText', + { defaultMessage: 'JSON blob containing the credentials and key used to subscribe' } +); + +type GcpCredentialsType = 'credentials_file' | 'credentials_json'; +type GcpFields = Record; +interface GcpInputFields { + fields: GcpFields; +} + +const gcpField: GcpInputFields = { + fields: { + project_id: { + label: i18n.translate('xpack.csp.gcpIntegration.projectidFieldLabel', { + defaultMessage: 'Project ID', + }), + type: 'text', + }, + credentials_file: { + label: i18n.translate('xpack.csp.gcpIntegration.credentialsFileFieldLabel', { + defaultMessage: 'Credentials File', + }), + type: 'text', + }, + credentials_json: { + label: i18n.translate('xpack.csp.gcpIntegration.credentialsJSONFieldLabel', { + defaultMessage: 'Credentials JSON', + }), + type: 'text', + }, + }, +}; + +const credentialOptionsList = [ + { + label: i18n.translate('xpack.csp.gcpIntegration.credentialsFileOption', { + defaultMessage: 'Credentials File', + }), + text: 'Credentials File', + }, + { + label: i18n.translate('xpack.csp.gcpIntegration.credentialsjsonOption', { + defaultMessage: 'Credentials JSON', + }), + text: 'Credentials JSON', + }, +]; + +const getSetupFormatOptions = (): Array<{ + id: SetupFormatGCP; + label: string; + disabled: boolean; +}> => [ + { + id: 'google_cloud_shell', + label: i18n.translate('xpack.csp.gcpIntegration.setupFormatOptions.googleCloudShell', { + defaultMessage: 'Google Cloud Shell', + }), + disabled: true, + }, + { + id: 'manual', + label: i18n.translate('xpack.csp.gcpIntegration.setupFormatOptions.manual', { + defaultMessage: 'Manual', + }), + disabled: false, + }, +]; + +interface Props { + newPolicy: NewPackagePolicy; + input: Extract< + NewPackagePolicyPostureInput, + { type: 'cloudbeat/cis_aws' | 'cloudbeat/cis_eks' | 'cloudbeat/cis_gcp' } + >; + updatePolicy(updatedPolicy: NewPackagePolicy): void; + packageInfo: PackageInfo; + setIsValid: (isValid: boolean) => void; + onChange: any; +} + +const getInputVarsFields = ( + input: NewPackagePolicyInput, + fields: GcpInputFields[keyof GcpInputFields] +) => + Object.entries(input.streams[0].vars || {}) + .filter(([id]) => id in fields) + .map(([id, inputVar]) => { + const field = fields[id]; + return { + id, + label: field.label, + type: field.type || 'text', + value: inputVar.value, + } as const; + }); + +export const GcpCredentialsForm = ({ + input, + newPolicy, + updatePolicy, + packageInfo, + setIsValid, + onChange, +}: Props) => { + const fields = getInputVarsFields(input, gcpField.fields); + + useEffect(() => { + const isInvalid = packageInfo.version < MIN_VERSION_GCP_CIS; + + setIsValid(!isInvalid); + + onChange({ + isValid: !isInvalid, + updatedPolicy: newPolicy, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [input, packageInfo]); + + if (packageInfo.version < MIN_VERSION_GCP_CIS) { + return ( + <> + + + + + + ); + } + return ( + <> + + + updatePolicy(getPosturePolicy(newPolicy, input.type))} + /> + + + updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } })) + } + /> + + {DocsLink} + + + ); +}; + +const GcpSetupAccessSelector = ({ onChange }: { onChange(type: GcpCredentialsType): void }) => ( + onChange(id)} + /> +); + +const GcpInputVarFields = ({ + fields, + onChange, +}: { + fields: Array; + onChange: (key: string, value: string) => void; +}) => { + const [credentialOption, setCredentialOption] = useState('Credentials File'); + const targetFieldName = (id: string) => { + return fields.find((element) => element.id === id); + }; + return ( +
+ + + onChange(targetFieldName('project_id')!.id, event.target.value)} + /> + + + { + setCredentialOption(optionElem.target.value); + }} + /> + + {credentialOption === 'Credentials File' && ( + + + onChange(targetFieldName('credentials_file')!.id, event.target.value) + } + /> + + )} + {credentialOption === 'Credentials JSON' && ( + + + onChange(targetFieldName('credentials_json')!.id, event.target.value) + } + /> + + )} + +
+ ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx index 7f7d4493b22d..0de4cddf9a29 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.test.tsx @@ -197,7 +197,7 @@ describe('', () => { expect(option2).toBeInTheDocument(); expect(option3).toBeInTheDocument(); expect(option1).toBeEnabled(); - expect(option2).toBeDisabled(); + expect(option2).toBeEnabled(); expect(option3).toBeDisabled(); expect(option1).toBeChecked(); }); diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_selectors.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_selectors.tsx index f234ce0d5e15..1ddc1cce0c56 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_selectors.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_selectors.tsx @@ -20,6 +20,7 @@ import { getPolicyTemplateInputOptions, type NewPackagePolicyPostureInput } from import { RadioGroup } from './csp_boxed_radio_group'; import { AwsCredentialsForm } from './aws_credentials_form/aws_credentials_form'; import { EksCredentialsForm } from './eks_credentials_form'; +import { GcpCredentialsForm } from './gcp_credential_form'; interface PolicyTemplateSelectorProps { selectedTemplate: CloudSecurityPolicyTemplate; @@ -79,6 +80,8 @@ export const PolicyTemplateVarsForm = ({ input, ...props }: PolicyTemplateVarsFo return ; case 'cloudbeat/cis_eks': return ; + case 'cloudbeat/cis_gcp': + return ; default: return null; } diff --git a/x-pack/plugins/fleet/common/index.ts b/x-pack/plugins/fleet/common/index.ts index 6d89be74ada2..df9ecae76bea 100644 --- a/x-pack/plugins/fleet/common/index.ts +++ b/x-pack/plugins/fleet/common/index.ts @@ -58,6 +58,7 @@ export { ENDPOINT_PRIVILEGES, // dashboards ids DASHBOARD_LOCATORS_IDS, + FLEET_ENROLLMENT_API_PREFIX, } from './constants'; export { // Route services diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents@action_status.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents@action_status.yaml index 89bc0d6c8917..c0e81da8331d 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agents@action_status.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agents@action_status.yaml @@ -8,7 +8,7 @@ get: - schema: type: integer default: 5 - in: query + in: query name: errorSize responses: '200': @@ -78,7 +78,7 @@ get: type: string description: policy id (POLICY_CHANGE action) revision: - type: string + type: string description: new policy revision (POLICY_CHANGE action) creationTime: type: string @@ -90,11 +90,11 @@ get: type: object properties: agentId: - type: string + type: string error: type: string timestamp: - type: string + type: string required: - actionId - complete diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 6c4108abf622..f8ed69d6d4d3 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -415,3 +415,9 @@ export interface FleetServerAgentAction { [k: string]: unknown; } + +export interface ActionStatusOptions { + errorSize: number; + page?: number; + perPage?: number; +} diff --git a/x-pack/plugins/fleet/common/types/rest_spec/common.ts b/x-pack/plugins/fleet/common/types/rest_spec/common.ts index cd3279b4cb25..07bd3c7019c1 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/common.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/common.ts @@ -7,9 +7,6 @@ import type { HttpFetchQuery } from '@kbn/core/public'; -/** - * @deprecated will be replaced by a "narrow" set of parameters - */ export interface ListWithKuery extends HttpFetchQuery { page?: number; perPage?: number; diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.test.tsx index e13c7006e36d..5478aca7c5ba 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.test.tsx @@ -208,14 +208,6 @@ describe('SearchBar', () => { }); describe('filterAndConvertFields', () => { - it('prepends the fieldPrefix if passed and hides some fields ', async () => { - expect(filterAndConvertFields(fields, '.test-index', 'test-index')).toEqual({ - 'test-index.api_key': { esTypes: ['keyword'], name: 'test-index.api_key', type: 'string' }, - 'test-index.name': { esTypes: ['keyword'], name: 'test-index.name', type: 'string' }, - 'test-index.version': { esTypes: ['keyword'], name: 'test-index.version', type: 'string' }, - }); - }); - it('leaves the fields names unchanged and does not hide any fields if fieldPrefix is not passed', async () => { expect(filterAndConvertFields(fields, '.test-index')).toEqual({ _id: { esTypes: ['_id'], name: '_id', type: 'string' }, diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx index f2a26126f363..5e610a46e3f3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx @@ -49,22 +49,14 @@ export const filterAndConvertFields = ( if (indexPattern === INDEX_NAME) { filteredFields = fields.filter((field) => field.name.startsWith(fieldPrefix)); } else { - // Concatenate the fields with the prefix - const withPrefix = fields.map((field) => { - return !field.name.startsWith(fieldPrefix) - ? { ...field, name: `${fieldPrefix}.${field.name}` } - : field; - }); // filter out fields that have names to be hidden - filteredFields = withPrefix.filter((field) => { - if (field.name.startsWith(fieldPrefix)) { - for (const hiddenField of HIDDEN_FIELDS) { - if (field.name.includes(hiddenField)) { - return false; - } + filteredFields = fields.filter((field) => { + for (const hiddenField of HIDDEN_FIELDS) { + if (field.name.includes(hiddenField)) { + return false; } - return true; } + return true; }); } } else { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx index 59f0df5045b3..68f0205a40c7 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/es_requirements_page.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiPageBody, - EuiPageContent_Deprecated as EuiPageContent, + EuiPageSection, EuiText, EuiSpacer, EuiIcon, @@ -22,6 +22,8 @@ import { EuiLink, } from '@elastic/eui'; +import styled from 'styled-components'; + import { WithoutHeaderLayout } from '../../../layouts'; import type { GetFleetStatusResponse } from '../../../types'; import { useStartServices } from '../../../hooks'; @@ -48,15 +50,21 @@ export const RequirementItem: React.FunctionComponent<{ isMissing: boolean }> = ); }; +const borderColor = '#d3dae6'; + +const StyledPageBody = styled(EuiPageBody)` + border: 1px solid ${borderColor}; + border-radius: 5px; +`; + export const MissingESRequirementsPage: React.FunctionComponent<{ missingRequirements: GetFleetStatusResponse['missing_requirements']; }> = ({ missingRequirements }) => { const { docLinks } = useStartServices(); - return ( - - + + - - + + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/error_pages/components/no_data_layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/error_pages/components/no_data_layout.tsx index 9d9703451dd7..c37bce702e97 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/error_pages/components/no_data_layout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/error_pages/components/no_data_layout.tsx @@ -5,36 +5,31 @@ * 2.0. */ -import { - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiPageContent_Deprecated as EuiPageContent, -} from '@elastic/eui'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiPageSection } from '@elastic/eui'; import React from 'react'; import { withRouter } from 'react-router-dom'; interface LayoutProps { title: string | React.ReactNode; actionSection?: React.ReactNode; - modalClosePath?: string; } export const NoDataLayout: React.FunctionComponent = withRouter< any, React.FunctionComponent ->(({ actionSection, title, modalClosePath, children }: React.PropsWithChildren) => { +>(({ actionSection, title, children }: React.PropsWithChildren) => { return ( - + {title}} body={children} actions={actionSection} /> - + ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/error_pages/enforce_security.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/error_pages/enforce_security.tsx deleted file mode 100644 index 74475aca10d5..000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/error_pages/enforce_security.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { FormattedMessage, injectI18n } from '@kbn/i18n-react'; - -import { NoDataLayout } from './components/no_data_layout'; - -export const EnforceSecurityPage = injectI18n(({ intl }) => ( - -

- -

-
-)); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/error_pages/invalid_license.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/error_pages/invalid_license.tsx deleted file mode 100644 index 66f4d5fca19c..000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/error_pages/invalid_license.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { FormattedMessage, injectI18n } from '@kbn/i18n-react'; - -import { NoDataLayout } from './components/no_data_layout'; - -export const InvalidLicensePage = injectI18n(({ intl }) => ( - -

- -

-
-)); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/use_fleet_server_unhealthy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/use_fleet_server_unhealthy.tsx index 3c7ba71199ea..5dd3fb9b5dfd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/use_fleet_server_unhealthy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/use_fleet_server_unhealthy.tsx @@ -37,7 +37,7 @@ export function useFleetServerUnhealthy() { if (agentPolicyIds.length > 0) { const agentStatusesRes = await sendGetAgentStatus({ - kuery: agentPolicyIds.map((policyId) => `policy_id:"${policyId}"`).join(' or '), + kuery: agentPolicyIds.map((policyId) => `policy_id:${policyId}`).join(' or '), }); if (agentStatusesRes.error) { diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 3635f8b38020..356aaa0e0cf8 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -95,3 +95,4 @@ export { } from './fleet_es_assets'; export { FILE_STORAGE_DATA_AGENT_INDEX } from './fleet_es_assets'; export { FILE_STORAGE_METADATA_AGENT_INDEX } from './fleet_es_assets'; +export * from './mappings'; diff --git a/x-pack/plugins/fleet/server/constants/mappings.ts b/x-pack/plugins/fleet/server/constants/mappings.ts new file mode 100644 index 000000000000..617a5d35a46d --- /dev/null +++ b/x-pack/plugins/fleet/server/constants/mappings.ts @@ -0,0 +1,332 @@ +/* + * 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. + */ + +/** + * The mappings declared closely mirror the ones declared in indices and SOs + * But they are only used to perform validation on those endpoints using ListWithKuery + * Whenever a field is added on any of these mappings, make sure to add it here as well + */ + +export const AGENT_POLICY_MAPPINGS = { + properties: { + agent_features: { + properties: { + name: { type: 'keyword' }, + enabled: { type: 'boolean' }, + }, + }, + data_output_id: { type: 'keyword' }, + description: { type: 'text' }, + download_source_id: { type: 'keyword' }, + fleet_server_host_id: { type: 'keyword' }, + inactivity_timeout: { type: 'integer' }, + is_default: { type: 'boolean' }, + is_default_fleet_server: { type: 'boolean' }, + is_managed: { type: 'boolean' }, + is_preconfigured: { type: 'keyword' }, + is_protected: { type: 'boolean' }, + monitoring_enabled: { type: 'keyword', index: false }, + monitoring_output_id: { type: 'keyword' }, + name: { type: 'keyword' }, + namespace: { type: 'keyword' }, + revision: { type: 'integer' }, + schema_version: { type: 'version' }, + status: { type: 'keyword' }, + unenroll_timeout: { type: 'integer' }, + updated_at: { type: 'date' }, + updated_by: { type: 'keyword' }, + }, +} as const; + +export const PACKAGE_POLICIES_MAPPINGS = { + properties: { + name: { type: 'keyword' }, + description: { type: 'text' }, + namespace: { type: 'keyword' }, + enabled: { type: 'boolean' }, + is_managed: { type: 'boolean' }, + policy_id: { type: 'keyword' }, + package: { + properties: { + name: { type: 'keyword' }, + title: { type: 'keyword' }, + version: { type: 'keyword' }, + }, + }, + elasticsearch: { + dynamic: false, + properties: {}, + }, + vars: { type: 'flattened' }, + inputs: { + dynamic: false, + properties: {}, + }, + secret_references: { properties: { id: { type: 'keyword' } } }, + revision: { type: 'integer' }, + updated_at: { type: 'date' }, + updated_by: { type: 'keyword' }, + created_at: { type: 'date' }, + created_by: { type: 'keyword' }, + }, +} as const; + +export const AGENT_MAPPINGS = { + properties: { + access_api_key_id: { + type: 'keyword', + }, + action_seq_no: { + type: 'integer', + }, + active: { + type: 'boolean', + }, + agent: { + properties: { + id: { + type: 'keyword', + }, + version: { + type: 'keyword', + }, + }, + }, + default_api_key: { + type: 'keyword', + }, + default_api_key_id: { + type: 'keyword', + }, + enrollment_id: { + type: 'keyword', + }, + enrolled_at: { + type: 'date', + }, + last_checkin: { + type: 'date', + }, + last_checkin_message: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + last_checkin_status: { + type: 'keyword', + }, + last_updated: { + type: 'date', + }, + local_metadata: { + properties: { + elastic: { + properties: { + agent: { + properties: { + build: { + properties: { + original: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + }, + }, + id: { + type: 'keyword', + }, + log_level: { + type: 'keyword', + }, + snapshot: { + type: 'boolean', + }, + upgradeable: { + type: 'boolean', + }, + version: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + }, + }, + }, + }, + host: { + properties: { + architecture: { + type: 'keyword', + }, + hostname: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + id: { + type: 'keyword', + }, + ip: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + mac: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + name: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + }, + }, + os: { + properties: { + family: { + type: 'keyword', + }, + full: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + kernel: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + name: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + platform: { + type: 'keyword', + }, + version: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + }, + }, + }, + }, + packages: { + type: 'keyword', + }, + policy_output_permissions_hash: { + type: 'keyword', + }, + policy_coordinator_idx: { + type: 'integer', + }, + policy_id: { + type: 'keyword', + }, + policy_revision_idx: { + type: 'integer', + }, + type: { + type: 'keyword', + }, + tags: { + type: 'keyword', + }, + unenrolled_at: { + type: 'date', + }, + unenrollment_started_at: { + type: 'date', + }, + unenrolled_reason: { + type: 'keyword', + }, + updated_at: { + type: 'date', + }, + upgrade_started_at: { + type: 'date', + }, + upgraded_at: { + type: 'date', + }, + upgrade_status: { + type: 'keyword', + }, + // added to allow validation on status field + status: { + type: 'keyword', + }, + }, +} as const; + +export const ENROLLMENT_API_KEY_MAPPINGS = { + properties: { + active: { + type: 'boolean', + }, + api_key: { + type: 'keyword', + }, + api_key_id: { + type: 'keyword', + }, + created_at: { + type: 'date', + }, + expire_at: { + type: 'date', + }, + name: { + type: 'keyword', + }, + policy_id: { + type: 'keyword', + }, + updated_at: { + type: 'date', + }, + }, +} as const; diff --git a/x-pack/plugins/fleet/server/integration_tests/cloud_preconfiguration.test.ts b/x-pack/plugins/fleet/server/integration_tests/cloud_preconfiguration.test.ts index 7da8edb68f9f..c6eaba98135f 100644 --- a/x-pack/plugins/fleet/server/integration_tests/cloud_preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/integration_tests/cloud_preconfiguration.test.ts @@ -164,201 +164,209 @@ describe('Fleet preconfiguration reset', () => { data.signed.data = ''; data.signed.signature = ''; - expect(data).toEqual({ - agent: { - download: { - sourceURI: 'https://artifacts.elastic.co/downloads/', - }, - features: {}, - monitoring: { - enabled: false, - logs: false, - metrics: false, - }, - protection: { - enabled: false, - signing_key: '', - uninstall_token_hash: '', - }, - }, - id: 'policy-elastic-agent-on-cloud', - inputs: [ - { - data_stream: { - namespace: 'default', + expect(data).toEqual( + expect.objectContaining({ + agent: { + download: { + sourceURI: 'https://artifacts.elastic.co/downloads/', }, - id: 'fleet-server-fleet_server-elastic-cloud-fleet-server', - meta: { - package: { - name: 'fleet_server', - }, + features: {}, + monitoring: { + enabled: false, + logs: false, + metrics: false, }, - name: 'Fleet Server', - package_policy_id: 'elastic-cloud-fleet-server', - revision: 1, - 'server.runtime': { - gc_percent: 20, + protection: { + enabled: false, + signing_key: '', + uninstall_token_hash: '', }, - type: 'fleet-server', - unused_key: 'not_used', - use_output: 'es-containerhost', }, - { - 'apm-server': { - agent: { - config: { - elasticsearch: { - api_key: '', - }, + id: 'policy-elastic-agent-on-cloud', + inputs: expect.arrayContaining([ + { + data_stream: { + namespace: 'default', + }, + id: 'fleet-server-fleet_server-elastic-cloud-fleet-server', + meta: { + package: { + name: 'fleet_server', }, }, - agent_config: [], - auth: { - anonymous: { - allow_agent: ['rum-js', 'js-base', 'iOS/swift'], - allow_service: null, + name: 'Fleet Server', + package_policy_id: 'elastic-cloud-fleet-server', + revision: 1, + 'server.runtime': { + gc_percent: 20, + }, + type: 'fleet-server', + unused_key: 'not_used', + use_output: 'es-containerhost', + }, + { + 'apm-server': { + agent: { + config: { + elasticsearch: { + api_key: '', + }, + }, + }, + agent_config: [], + auth: { + anonymous: { + allow_agent: ['rum-js', 'js-base', 'iOS/swift'], + allow_service: null, + enabled: true, + rate_limit: { + event_limit: 300, + ip_limit: 1000, + }, + }, + api_key: { + enabled: true, + limit: 100, + }, + secret_token: 'CLOUD_SECRET_TOKEN', + }, + capture_personal_data: true, + default_service_environment: null, + 'expvar.enabled': false, + host: '0.0.0.0:8200', + idle_timeout: '45s', + java_attacher: { + 'discovery-rules': null, + 'download-agent-version': null, + enabled: false, + }, + max_connections: 0, + max_event_size: 307200, + max_header_size: 1048576, + 'pprof.enabled': false, + read_timeout: '3600s', + response_headers: null, + rum: { + allow_headers: null, + allow_origins: ['*'], enabled: true, - rate_limit: { - event_limit: 300, - ip_limit: 1000, + exclude_from_grouping: '^/webpack', + library_pattern: 'node_modules|bower_components|~', + response_headers: null, + source_mapping: { + elasticsearch: { + api_key: '', + }, + metadata: [], + }, + }, + sampling: { + tail: { + enabled: false, + interval: '1m', + policies: [ + { + sample_rate: 0.1, + }, + ], + storage_limit: '3GB', }, }, - api_key: { + shutdown_timeout: '30s', + ssl: { + certificate: '/app/config/certs/node.crt', + cipher_suites: null, + curve_types: null, enabled: true, - limit: 100, + key: '/app/config/certs/node.key', + key_passphrase: null, + supported_protocols: ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], }, - secret_token: 'CLOUD_SECRET_TOKEN', + write_timeout: '30s', }, - capture_personal_data: true, - default_service_environment: null, - 'expvar.enabled': false, - host: '0.0.0.0:8200', - idle_timeout: '45s', - java_attacher: { - 'discovery-rules': null, - 'download-agent-version': null, - enabled: false, + data_stream: { + namespace: 'default', }, - max_connections: 0, - max_event_size: 307200, - max_header_size: 1048576, - 'pprof.enabled': false, - read_timeout: '3600s', - response_headers: null, - rum: { - allow_headers: null, - allow_origins: ['*'], - enabled: true, - exclude_from_grouping: '^/webpack', - library_pattern: 'node_modules|bower_components|~', - response_headers: null, - source_mapping: { - elasticsearch: { - api_key: '', - }, - metadata: [], + id: 'elastic-cloud-apm', + meta: { + package: { + name: 'apm', }, }, - sampling: { - tail: { - enabled: false, - interval: '1m', - policies: [ - { - sample_rate: 0.1, - }, - ], - storage_limit: '3GB', - }, + name: 'Elastic APM', + package_policy_id: 'elastic-cloud-apm', + revision: 2, + type: 'apm', + use_output: 'es-containerhost', + }, + ]), + output_permissions: { + 'es-containerhost': { + _elastic_agent_checks: { + cluster: ['monitor'], }, - shutdown_timeout: '30s', - ssl: { - certificate: '/app/config/certs/node.crt', - cipher_suites: null, - curve_types: null, - enabled: true, - key: '/app/config/certs/node.key', - key_passphrase: null, - supported_protocols: ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + _elastic_agent_monitoring: { + indices: [], }, - write_timeout: '30s', - }, - data_stream: { - namespace: 'default', - }, - id: 'elastic-cloud-apm', - meta: { - package: { - name: 'apm', + 'elastic-cloud-apm': { + cluster: ['cluster:monitor/main'], + indices: [ + { + names: ['logs-apm.app-default'], + privileges: ['auto_configure', 'create_doc'], + }, + { + names: ['metrics-apm.app.*-default'], + privileges: ['auto_configure', 'create_doc'], + }, + { + names: ['logs-apm.error-default'], + privileges: ['auto_configure', 'create_doc'], + }, + { + names: ['metrics-apm.internal-default'], + privileges: ['auto_configure', 'create_doc'], + }, + { + names: ['metrics-apm.profiling-default'], + privileges: ['auto_configure', 'create_doc'], + }, + { + names: ['traces-apm.rum-default'], + privileges: ['auto_configure', 'create_doc'], + }, + { + names: ['traces-apm.sampled-default'], + privileges: [ + 'auto_configure', + 'create_doc', + 'maintenance', + 'monitor', + 'read', + ], + }, + { + names: ['traces-apm-default'], + privileges: ['auto_configure', 'create_doc'], + }, + ], }, }, - name: 'Elastic APM', - package_policy_id: 'elastic-cloud-apm', - revision: 2, - type: 'apm', - use_output: 'es-containerhost', }, - ], - output_permissions: { - 'es-containerhost': { - _elastic_agent_checks: { - cluster: ['monitor'], - }, - _elastic_agent_monitoring: { - indices: [], - }, - 'elastic-cloud-apm': { - cluster: ['cluster:monitor/main'], - indices: [ - { - names: ['logs-apm.app-default'], - privileges: ['auto_configure', 'create_doc'], - }, - { - names: ['metrics-apm.app.*-default'], - privileges: ['auto_configure', 'create_doc'], - }, - { - names: ['logs-apm.error-default'], - privileges: ['auto_configure', 'create_doc'], - }, - { - names: ['metrics-apm.internal-default'], - privileges: ['auto_configure', 'create_doc'], - }, - { - names: ['metrics-apm.profiling-default'], - privileges: ['auto_configure', 'create_doc'], - }, - { - names: ['traces-apm.rum-default'], - privileges: ['auto_configure', 'create_doc'], - }, - { - names: ['traces-apm.sampled-default'], - privileges: ['auto_configure', 'create_doc', 'maintenance', 'monitor', 'read'], - }, - { - names: ['traces-apm-default'], - privileges: ['auto_configure', 'create_doc'], - }, - ], + outputs: { + 'es-containerhost': { + hosts: ['https://cloudinternales:9200'], + type: 'elasticsearch', }, }, - }, - outputs: { - 'es-containerhost': { - hosts: ['https://cloudinternales:9200'], - type: 'elasticsearch', + revision: 5, + secret_references: [], + signed: { + data: '', + signature: '', }, - }, - revision: 5, - secret_references: [], - signed: { - data: '', - signature: '', - }, - }); + }) + ); }); it('Create correct package policies', async () => { diff --git a/x-pack/plugins/fleet/server/routes/utils/filter_utils.test.ts b/x-pack/plugins/fleet/server/routes/utils/filter_utils.test.ts new file mode 100644 index 000000000000..f888959b152c --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/utils/filter_utils.test.ts @@ -0,0 +1,444 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as esKuery from '@kbn/es-query'; + +import { validateFilterKueryNode } from './filter_utils'; + +const mockMappings = { + properties: { + updated_at: { + type: 'date', + }, + foo: { + properties: { + title: { + type: 'text', + }, + description: { + type: 'text', + }, + bytes: { + type: 'integer', + }, + }, + }, + bar: { + properties: { + _id: { + type: 'keyword', + }, + foo: { + type: 'text', + }, + description: { + type: 'text', + }, + }, + }, + bean: { + properties: { + canned: { + fields: { + text: { + type: 'text', + }, + }, + type: 'keyword', + }, + }, + }, + alert: { + properties: { + actions: { + type: 'nested', + properties: { + group: { + type: 'keyword', + }, + actionRef: { + type: 'keyword', + }, + actionTypeId: { + type: 'keyword', + }, + params: { + enabled: false, + type: 'object', + }, + }, + }, + params: { + type: 'flattened', + }, + }, + }, + hiddenType: { + properties: { + description: { + type: 'text', + }, + }, + }, + }, +} as const; + +describe('Filter Utils', () => { + describe('ValidateFilterKueryNode', () => { + describe('Validate general kueries through KueryNode', () => { + it('Simple filter', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'foo.updated_at: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + types: ['foo'], + indexMapping: mockMappings, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'foo.updated_at', + type: 'foo', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.2', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.3', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + + it('Nested filter query', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'alert.attributes.actions:{ actionTypeId: ".server-log" }' + ), + types: ['alert'], + indexMapping: mockMappings, + hasNestedKey: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'alert.attributes.actions.actionTypeId', + type: 'alert', + }, + ]); + }); + + it('Accept defined key even if not wrapped by a saved object type', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'updated_at: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + types: ['foo'], + indexMapping: mockMappings, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'updated_at', + type: null, + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.2', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.3', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + + it('Return Error if key of a saved object type is not wrapped with attributes', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'foo.updated_at: 5678654567 and foo.attributes.bytes > 1000 and foo.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.description :*)' + ), + types: ['foo'], + indexMapping: mockMappings, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'foo.updated_at', + type: 'foo', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.2', + error: + "This key 'foo.bytes' does NOT match the filter proposition SavedObjectType.attributes.key", + isSavedObjectAttr: false, + key: 'foo.bytes', + type: 'foo', + }, + { + astPath: 'arguments.3', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.1', + error: + "This key 'foo.description' does NOT match the filter proposition SavedObjectType.attributes.key", + isSavedObjectAttr: false, + key: 'foo.description', + type: 'foo', + }, + ]); + }); + + it('Return Error if filter is not using an allowed type', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'bar.updated_at: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + types: ['foo'], + indexMapping: mockMappings, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: `This key 'bar.updated_at' does NOT exist in foo saved object index patterns`, + isSavedObjectAttr: true, + key: 'bar.updated_at', + type: 'bar', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.2', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.3', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + + it('Return Error if filter is using an non-existing key in the index patterns of the saved object type', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'foo.updated_at33: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.header: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + types: ['foo'], + indexMapping: mockMappings, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: "This key 'foo.updated_at33' does NOT exist in foo saved object index patterns", + isSavedObjectAttr: false, + key: 'foo.updated_at33', + type: 'foo', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.2', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.3', + error: + "This key 'foo.attributes.header' does NOT exist in foo saved object index patterns", + isSavedObjectAttr: false, + key: 'foo.attributes.header', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + + it('Return Error if filter is using an non-existing key null key', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression('foo.attributes.description: hello AND bye'), + types: ['foo'], + indexMapping: mockMappings, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.1', + error: 'The key is empty and needs to be wrapped by a saved object type like foo', + isSavedObjectAttr: false, + key: null, + type: null, + }, + ]); + }); + + it('Multiple nested filter queries', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'alert.attributes.actions:{ actionTypeId: ".server-log" AND actionRef: "foo" }' + ), + types: ['alert'], + indexMapping: mockMappings, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'alert.attributes.actions.actionTypeId', + type: 'alert', + }, + { + astPath: 'arguments.1.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'alert.attributes.actions.actionRef', + type: 'alert', + }, + ]); + }); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/routes/utils/filter_utils.ts b/x-pack/plugins/fleet/server/routes/utils/filter_utils.ts new file mode 100644 index 000000000000..fccddd66891c --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/utils/filter_utils.ts @@ -0,0 +1,255 @@ +/* + * 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 { get } from 'lodash'; +import * as esKuery from '@kbn/es-query'; +import type { IndexMapping } from '@kbn/core-saved-objects-base-server-internal'; + +type KueryNode = any; + +const astFunctionType = ['is', 'range', 'nested']; +const allowedTerms = ['_exists_']; + +interface ValidateFilterKueryNode { + astPath: string; + error: string; + isSavedObjectAttr: boolean; + key: string; + type: string | null; +} + +interface ValidateFilterKueryNodeParams { + astFilter: KueryNode; + types: string[]; + indexMapping: IndexMapping; + hasNestedKey?: boolean; + nestedKeys?: string; + storeValue?: boolean; + path?: string; + skipNormalization?: boolean; +} + +export const validateFilterKueryNode = ({ + astFilter, + types, + indexMapping, + hasNestedKey = false, + nestedKeys, + storeValue = false, + path = 'arguments', + skipNormalization, +}: ValidateFilterKueryNodeParams): ValidateFilterKueryNode[] => { + let localNestedKeys: string | undefined; + return astFilter.arguments.reduce((kueryNode: string[], ast: KueryNode, index: number) => { + if (hasNestedKey && ast.type === 'literal' && ast.value != null) { + localNestedKeys = ast.value; + } else if (ast.type === 'literal' && ast.value && typeof ast.value === 'string') { + const key = ast.value.replace('.attributes', ''); + const mappingKey = 'properties.' + key.split('.').join('.properties.'); + const field = get(indexMapping, mappingKey); + + if (field != null && field.type === 'nested') { + localNestedKeys = ast.value; + } + } + + if (ast.arguments) { + const myPath = `${path}.${index}`; + return [ + ...kueryNode, + ...validateFilterKueryNode({ + astFilter: ast, + types, + indexMapping, + storeValue: ast.type === 'function' && astFunctionType.includes(ast.function), + path: `${myPath}.arguments`, + hasNestedKey: ast.type === 'function' && ast.function === 'nested', + nestedKeys: localNestedKeys || nestedKeys, + skipNormalization, + }), + ]; + } + if (storeValue && index === 0) { + const splitPath = path.split('.'); + const astPath = path.includes('.') + ? splitPath.slice(0, splitPath.length - 1).join('.') + : `${path}.${index}`; + const key = nestedKeys != null ? `${nestedKeys}.${ast.value}` : ast.value; + + return [ + ...kueryNode, + { + astPath, + error: hasFilterKeyError(key, types, indexMapping, skipNormalization), + isSavedObjectAttr: isSavedObjectAttr(key, indexMapping), + key, + type: getType(key), + }, + ]; + } + return kueryNode; + }, []); +}; + +const getType = (key: string | undefined | null) => { + if (key != null && key.includes('.')) { + return key.split('.')[0]; + } else if (allowedTerms.some((term) => term === key)) { + return 'searchTerm'; + } else { + return null; + } +}; + +/** + * Is this filter key referring to a a top-level SavedObject attribute such as + * `updated_at` or `references`. + * + * @param key + * @param indexMapping + */ +export const isSavedObjectAttr = (key: string | null | undefined, indexMapping: IndexMapping) => { + const keySplit = key != null ? key.split('.') : []; + if (keySplit.length === 1 && fieldDefined(indexMapping, keySplit[0])) { + return true; + } else if (keySplit.length === 2 && keySplit[1] === 'id') { + return true; + } else if (keySplit.length === 2 && fieldDefined(indexMapping, keySplit[1])) { + return true; + } else { + return false; + } +}; + +export const hasFilterKeyError = ( + key: string | null | undefined, + types: string[], + indexMapping: IndexMapping, + skipNormalization?: boolean +): string | null => { + if (key == null) { + return `The key is empty and needs to be wrapped by a saved object type like ${types.join()}`; + } + if (!key.includes('.')) { + if (allowedTerms.some((term) => term === key) || fieldDefined(indexMapping, key)) { + return null; + } + return `This type '${key}' is not allowed`; + } else if (key.includes('.')) { + const keySplit = key.split('.'); + if ( + keySplit.length <= 1 && + !fieldDefined(indexMapping, keySplit[0]) && + !types.includes(keySplit[0]) + ) { + return `This type '${keySplit[0]}' is not allowed`; + } + // In some cases we don't want to check about the `attributes` presence + // In that case pass the `skipNormalization` parameter + if ( + (!skipNormalization && keySplit.length === 2 && fieldDefined(indexMapping, key)) || + (!skipNormalization && keySplit.length > 2 && keySplit[1] !== 'attributes') + ) { + return `This key '${key}' does NOT match the filter proposition SavedObjectType.attributes.key`; + } + // Check that the key exists in the mappings + const searchKey = + skipNormalization || keySplit[1] !== 'attributes' + ? `${keySplit[0]}.${keySplit.slice(1, keySplit.length).join('.')}` + : `${keySplit[0]}.${keySplit.slice(2, keySplit.length).join('.')}`; + if ( + (keySplit.length === 2 && !fieldDefined(indexMapping, keySplit[1])) || + (keySplit.length === 2 && + !types.includes(keySplit[0]) && + !fieldDefined(indexMapping, searchKey)) || + (keySplit.length > 2 && !fieldDefined(indexMapping, searchKey)) + ) { + return `This key '${key}' does NOT exist in ${types.join()} saved object index patterns`; + } + } + return null; +}; + +export const fieldDefined = (indexMappings: IndexMapping, key: string): boolean => { + const keySplit = key.split('.'); + const shortenedKey = `${keySplit[1]}.${keySplit.slice(2, keySplit.length).join('.')}`; + const mappingKey = 'properties.' + key.split('.').join('.properties.'); + const shortenedMappingKey = 'properties.' + shortenedKey.split('.').join('.properties.'); + + if (get(indexMappings, mappingKey) != null || get(indexMappings, shortenedMappingKey) != null) { + return true; + } + + if (mappingKey === 'properties.id') { + return true; + } + + // If the `mappingKey` does not match a valid path, before returning false, + // we want to check and see if the intended path was for a multi-field + // such as `x.attributes.field.text` where `field` is mapped to both text + // and keyword + const propertiesAttribute = 'properties'; + const indexOfLastProperties = mappingKey.lastIndexOf(propertiesAttribute); + const fieldMapping = mappingKey.substr(0, indexOfLastProperties); + const fieldType = mappingKey.substr( + mappingKey.lastIndexOf(propertiesAttribute) + `${propertiesAttribute}.`.length + ); + const mapping = `${fieldMapping}fields.${fieldType}`; + if (get(indexMappings, mapping) != null) { + return true; + } + + // If the path is for a flattened type field, we'll assume the mappings are defined. + const keys = key.split('.'); + for (let i = 0; i < keys.length; i++) { + const path = `properties.${keys.slice(0, i + 1).join('.properties.')}`; + if (get(indexMappings, path)?.type === 'flattened') { + return true; + } + } + + return false; +}; + +export const validateKuery = ( + kuery: string | undefined, + allowedTypes: string[], + indexMapping: IndexMapping, + skipNormalization?: boolean +) => { + let isValid = true; + let error: string | undefined; + + if (!kuery) { + isValid = true; + } + try { + if (kuery && indexMapping) { + const astFilter = esKuery.fromKueryExpression(kuery); + const validationObject = validateFilterKueryNode({ + astFilter, + types: allowedTypes, + indexMapping, + storeValue: true, + skipNormalization, + }); + if (validationObject.some((obj) => obj.error != null)) { + error = `KQLSyntaxError: ${validationObject + .filter((obj) => obj.error != null) + .map((obj) => obj.error) + .join('\n')}`; + isValid = false; + } + } else { + isValid = true; + } + return { isValid, error }; + } catch (e) { + isValid = false; + error = e.message; + } +}; diff --git a/x-pack/plugins/fleet/server/routes/utils/filter_utils_real_queries.test.ts b/x-pack/plugins/fleet/server/routes/utils/filter_utils_real_queries.test.ts new file mode 100644 index 000000000000..e8d3b6559880 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/utils/filter_utils_real_queries.test.ts @@ -0,0 +1,755 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as esKuery from '@kbn/es-query'; + +import { + AGENT_POLICY_SAVED_OBJECT_TYPE, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + AGENTS_PREFIX, + AGENT_POLICY_MAPPINGS, + PACKAGE_POLICIES_MAPPINGS, + AGENT_MAPPINGS, + ENROLLMENT_API_KEY_MAPPINGS, +} from '../../constants'; + +import { FLEET_ENROLLMENT_API_PREFIX } from '../../../common/constants'; + +import { validateFilterKueryNode, validateKuery } from './filter_utils'; + +describe('ValidateFilterKueryNode validates real kueries through KueryNode', () => { + describe('Agent policies', () => { + it('Test 1 - search by data_output_id', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id: test_id` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENT_POLICY_SAVED_OBJECT_TYPE], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'ingest-agent-policies.data_output_id', + type: 'ingest-agent-policies', + }, + ]); + }); + + it('Test 2 - search by inactivity timeout', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.inactivity_timeout:*` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENT_POLICY_SAVED_OBJECT_TYPE], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'ingest-agent-policies.inactivity_timeout', + type: 'ingest-agent-policies', + }, + ]); + }); + + it('Test 3 - complex query', async () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.download_source_id:some_id or (not ${AGENT_POLICY_SAVED_OBJECT_TYPE}.download_source_id:*)` + ), + types: [AGENT_POLICY_SAVED_OBJECT_TYPE], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'ingest-agent-policies.download_source_id', + type: 'ingest-agent-policies', + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'ingest-agent-policies.download_source_id', + type: 'ingest-agent-policies', + }, + ]); + }); + + it('Test 4', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id: test_id or ${AGENT_POLICY_SAVED_OBJECT_TYPE}.monitoring_output_id: test_id or (not ${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id:*)` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENT_POLICY_SAVED_OBJECT_TYPE], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'ingest-agent-policies.data_output_id', + type: 'ingest-agent-policies', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: true, + key: 'ingest-agent-policies.monitoring_output_id', + type: 'ingest-agent-policies', + }, + { + astPath: 'arguments.2.arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'ingest-agent-policies.data_output_id', + type: 'ingest-agent-policies', + }, + ]); + }); + + it('Test 5 - returns error if the attribute does not exist', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies:test_id_1 or ${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies:test_id_2` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENT_POLICY_SAVED_OBJECT_TYPE], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: + "This key 'ingest-agent-policies.package_policies' does NOT exist in ingest-agent-policies saved object index patterns", + isSavedObjectAttr: false, + key: 'ingest-agent-policies.package_policies', + type: 'ingest-agent-policies', + }, + { + astPath: 'arguments.1', + error: + "This key 'ingest-agent-policies.package_policies' does NOT exist in ingest-agent-policies saved object index patterns", + isSavedObjectAttr: false, + key: 'ingest-agent-policies.package_policies', + type: 'ingest-agent-policies', + }, + ]); + }); + }); + + describe('Package policies', () => { + it('Search by package name', async () => { + const astFilter = esKuery.fromKueryExpression( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name:packageName` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + indexMapping: PACKAGE_POLICIES_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'ingest-package-policies.attributes.package.name', + type: 'ingest-package-policies', + }, + ]); + }); + + it('It fails if the kuery is not normalized', async () => { + const astFilter = esKuery.fromKueryExpression( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:packageName` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + indexMapping: PACKAGE_POLICIES_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: + "This key 'ingest-package-policies.package.name' does NOT match the filter proposition SavedObjectType.attributes.key", + isSavedObjectAttr: false, + key: 'ingest-package-policies.package.name', + type: 'ingest-package-policies', + }, + ]); + }); + + it('It does not check attributes if skipNormalization is passed', async () => { + const astFilter = esKuery.fromKueryExpression( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:packageName` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + indexMapping: PACKAGE_POLICIES_MAPPINGS, + storeValue: true, + skipNormalization: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'ingest-package-policies.package.name', + type: 'ingest-package-policies', + }, + ]); + }); + + it('Allows passing query without SO', async () => { + const astFilter = esKuery.fromKueryExpression(`package.name:packageName`); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + indexMapping: PACKAGE_POLICIES_MAPPINGS, + storeValue: true, + skipNormalization: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'package.name', + type: 'package', + }, + ]); + }); + }); + + describe('Agents', () => { + it('Search policy id', async () => { + const astFilter = esKuery.fromKueryExpression(`${AGENTS_PREFIX}.policy_id: "policy_id"`); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENTS_PREFIX], + indexMapping: AGENT_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.policy_id', + type: 'fleet-agents', + }, + ]); + }); + + it('Search by multiple ids', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENTS_PREFIX}.attributes.agent.id : (id_1 or id_2)` + ); + + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENTS_PREFIX], + indexMapping: AGENT_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'fleet-agents.attributes.agent.id', + type: 'fleet-agents', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'fleet-agents.attributes.agent.id', + type: 'fleet-agents', + }, + ]); + }); + + it('Search agent by policy Id and enrolled since more than 10m', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENTS_PREFIX}.policy_id: "policyId" and not (_exists_: "${AGENTS_PREFIX}.unenrolled_at") and ${AGENTS_PREFIX}.enrolled_at >= now-10m` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENTS_PREFIX], + indexMapping: AGENT_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.policy_id', + type: 'fleet-agents', + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: '_exists_', + type: 'searchTerm', + }, + { + astPath: 'arguments.2', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.enrolled_at', + type: 'fleet-agents', + }, + ]); + }); + + it('Search agent by multiple policy Ids and tags', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENTS_PREFIX}.policy_id: (policyId1 or policyId2) and ${AGENTS_PREFIX}.tags: (tag1)` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENTS_PREFIX], + indexMapping: AGENT_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0.arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.policy_id', + type: 'fleet-agents', + }, + { + astPath: 'arguments.0.arguments.1', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.policy_id', + type: 'fleet-agents', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.tags', + type: 'fleet-agents', + }, + ]); + }); + + it('Search agent by multiple tags', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENTS_PREFIX}.tags: (tag1 or tag2 or tag3)` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENTS_PREFIX], + indexMapping: AGENT_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.tags', + type: 'fleet-agents', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.tags', + type: 'fleet-agents', + }, + { + astPath: 'arguments.2', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.tags', + type: 'fleet-agents', + }, + ]); + }); + + it('Returns error if kuery is passed without a reference to the index', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENTS_PREFIX}.status:online or (${AGENTS_PREFIX}.status:updating or ${AGENTS_PREFIX}.status:unenrolling or ${AGENTS_PREFIX}.status:enrolling)` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENTS_PREFIX], + indexMapping: AGENT_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.status', + type: 'fleet-agents', + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.status', + type: 'fleet-agents', + }, + { + astPath: 'arguments.1.arguments.1', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.status', + type: 'fleet-agents', + }, + { + astPath: 'arguments.1.arguments.2', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.status', + type: 'fleet-agents', + }, + ]); + }); + }); + + describe('Enrollment Api keys', () => { + it('Search by policy id', async () => { + const astFilter = esKuery.fromKueryExpression( + `${FLEET_ENROLLMENT_API_PREFIX}.policy_id: policyId1` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [FLEET_ENROLLMENT_API_PREFIX], + indexMapping: ENROLLMENT_API_KEY_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'fleet-enrollment-api-keys.policy_id', + type: 'fleet-enrollment-api-keys', + }, + ]); + }); + }); +}); + +describe('validateKuery validates real kueries', () => { + describe('Agent policies', () => { + it('Search by data_output_id', async () => { + const validationObj = validateKuery( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id: test_id`, + [AGENT_POLICY_SAVED_OBJECT_TYPE], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by data_output_id without SO wrapping', async () => { + const validationObj = validateKuery( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id: test_id`, + [AGENT_POLICY_SAVED_OBJECT_TYPE], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by name', async () => { + const validationObj = validateKuery( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.name: test_id`, + [AGENT_POLICY_SAVED_OBJECT_TYPE], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Invalid kuery', async () => { + const validationObj = validateKuery( + 'test%3A', + [AGENT_POLICY_SAVED_OBJECT_TYPE], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toContain( + `KQLSyntaxError: The key is empty and needs to be wrapped by a saved object type like ingest-agent-policies` + ); + }); + + it('Kuery with non existent parameter wrapped by SO', async () => { + const validationObj = validateKuery( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.non_existent_parameter: 'test_id'`, + [AGENT_POLICY_SAVED_OBJECT_TYPE], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toContain( + `KQLSyntaxError: This key 'ingest-agent-policies.non_existent_parameter' does NOT exist in ingest-agent-policies saved object index patterns` + ); + }); + + it('Kuery with non existent parameter', async () => { + const validationObj = validateKuery( + `non_existent_parameter: 'test_id'`, + [AGENT_POLICY_SAVED_OBJECT_TYPE], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toContain( + `KQLSyntaxError: This type 'non_existent_parameter' is not allowed` + ); + }); + }); + + describe('Agents', () => { + it('Test 1 - search policy id', async () => { + const validationObj = validateKuery( + `${AGENTS_PREFIX}.policy_id: "policy_id"`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Test 2 - status kuery without SO wrapping', async () => { + const validationObj = validateKuery( + `status:online or (status:updating or status:unenrolling or status:enrolling)`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Test 3 - status kuery with SO wrapping', async () => { + const validationObj = validateKuery( + `${AGENTS_PREFIX}.status:online or (${AGENTS_PREFIX}.status:updating or ${AGENTS_PREFIX}.status:unenrolling or ${AGENTS_PREFIX}.status:enrolling)`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Test 4 - valid kuery without SO wrapping', async () => { + const validationObj = validateKuery( + `local_metadata.elastic.agent.version : "8.6.0"`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by multiple agent ids', async () => { + const validationObj = validateKuery( + `${AGENTS_PREFIX}.agent.id : (id_1 or id_2)`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by complex query', async () => { + const validationObj = validateKuery( + `${AGENTS_PREFIX}.policy_id: "policyId" and not (_exists_: "${AGENTS_PREFIX}.unenrolled_at") and ${AGENTS_PREFIX}.enrolled_at >= now-10m`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by complex query without SO wrapping', async () => { + const validationObj = validateKuery( + `policy_id: "policyId" and not (_exists_: "unenrolled_at") and enrolled_at >= now-10m`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by tags', async () => { + const validationObj = validateKuery( + `${AGENTS_PREFIX}.tags: (tag1 or tag2 or tag3)`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by hostname keyword and status', async () => { + const validationObj = validateKuery( + `(${AGENTS_PREFIX}.local_metadata.host.hostname.keyword:test) and (${AGENTS_PREFIX}.status:online)`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by deeply nested fields', async () => { + const validationObj = validateKuery( + `${AGENTS_PREFIX}.local_metadata.os.version.keyword: test`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by deeply nested fields in local_metadata', async () => { + const validationObj = validateKuery( + `${AGENTS_PREFIX}.local_metadata.elastic.agent.build.original.keyword: test`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + }); + + describe('Package policies', () => { + it('Search by package name without SO', async () => { + const validationObj = validateKuery( + `package.name:fleet_server`, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by package name', async () => { + const validationObj = validateKuery( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:fleet_server`, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by package name works with attributes if skipNormalization is not passed', async () => { + const validationObj = validateKuery( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name:packageName`, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by name and version', async () => { + const validationObj = validateKuery( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "TestName" AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.version: "8.8.0"`, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Invalid search by nested wrong parameter', async () => { + const validationObj = validateKuery( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.is_managed:packageName`, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toEqual( + `KQLSyntaxError: This key 'ingest-package-policies.package.is_managed' does NOT exist in ingest-package-policies saved object index patterns` + ); + }); + + it('invalid search by nested wrong parameter - without wrapped SO', async () => { + const validationObj = validateKuery( + `package.is_managed:packageName`, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toEqual( + `KQLSyntaxError: This key 'package.is_managed' does NOT exist in ingest-package-policies saved object index patterns` + ); + }); + + it('Invalid search by non existent parameter', async () => { + const validationObj = validateKuery( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.non_existent_parameter:packageName`, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toEqual( + `KQLSyntaxError: This key 'ingest-package-policies.non_existent_parameter' does NOT exist in ingest-package-policies saved object index patterns` + ); + }); + }); + + describe('Enrollment keys', () => { + it('Search by policy id without SO name', async () => { + const validationObj = validateKuery( + `policy_id: policyId1`, + [FLEET_ENROLLMENT_API_PREFIX], + ENROLLMENT_API_KEY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by policy id', async () => { + const validationObj = validateKuery( + `${FLEET_ENROLLMENT_API_PREFIX}.policy_id: policyId1`, + [FLEET_ENROLLMENT_API_PREFIX], + ENROLLMENT_API_KEY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/agents/action_status.ts b/x-pack/plugins/fleet/server/services/agents/action_status.ts index 2872a1671a8f..403ddf02a034 100644 --- a/x-pack/plugins/fleet/server/services/agents/action_status.ts +++ b/x-pack/plugins/fleet/server/services/agents/action_status.ts @@ -13,8 +13,8 @@ import type { FleetServerAgentAction, ActionStatus, ActionErrorResult, - ListWithKuery, AgentActionType, + ActionStatusOptions, } from '../../types'; import { AGENT_ACTIONS_INDEX, @@ -29,7 +29,7 @@ import { appContextService } from '..'; */ export async function getActionStatuses( esClient: ElasticsearchClient, - options: ListWithKuery & { errorSize: number } + options: ActionStatusOptions ): Promise { const actions = await _getActions(esClient, options); const cancelledActions = await getCancelledActions(esClient); @@ -218,7 +218,7 @@ export async function getCancelledActions( async function _getActions( esClient: ElasticsearchClient, - options: ListWithKuery + options: ActionStatusOptions ): Promise { const res = await esClient.search({ index: AGENT_ACTIONS_INDEX, diff --git a/x-pack/plugins/fleet/server/services/saved_object.ts b/x-pack/plugins/fleet/server/services/saved_object.ts index 43ec734edceb..0a7a74be7052 100644 --- a/x-pack/plugins/fleet/server/services/saved_object.ts +++ b/x-pack/plugins/fleet/server/services/saved_object.ts @@ -5,11 +5,6 @@ * 2.0. */ -import type { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server'; - -import { SO_SEARCH_LIMIT } from '../constants'; -import type { ListWithKuery } from '../types'; - /** * Escape a value with double quote to use with saved object search * Example: escapeSearchQueryPhrase('-test"toto') => '"-test\"toto""' @@ -29,53 +24,3 @@ export const normalizeKuery = (savedObjectType: string, kuery: string): string = `${savedObjectType}.attributes.` ); }; - -// Like saved object client `.find()`, but ignores `page` and `perPage` parameters and -// returns *all* matching saved objects by collocating results from all `.find` pages. -// This function actually doesn't offer any additional benefits over `.find()` for now -// due to SO client limitations (see comments below), so is a placeholder for when SO -// client is improved. -export const findAllSOs = async ( - soClient: SavedObjectsClientContract, - options: Omit & { - type: string; - } -): Promise, 'saved_objects' | 'total'>> => { - const { type, sortField, sortOrder, kuery } = options; - let savedObjectResults: SavedObjectsFindResponse['saved_objects'] = []; - - const query = { - type, - sortField, - sortOrder, - filter: kuery, - page: 1, - perPage: SO_SEARCH_LIMIT, - }; - - const { saved_objects: initialSOs, total } = await soClient.find(query); - - savedObjectResults = initialSOs; - - // The saved object client can't actually page through more than the first 10,000 - // results, due to the same `index.max_result_window` constraint. The commented out - // code below is an example of paging through rest of results when the SO client - // offers that kind of support. - // if (total > searchLimit) { - // const remainingPages = Math.ceil((total - searchLimit) / searchLimit); - // for (let currentPage = 2; currentPage <= remainingPages + 1; currentPage++) { - // const { saved_objects: currentPageSavedObjects } = await soClient.find({ - // ...query, - // page: currentPage, - // }); - // if (currentPageSavedObjects.length) { - // savedObjectResults = savedObjectResults.concat(currentPageSavedObjects); - // } - // } - // } - - return { - saved_objects: savedObjectResults, - total, - }; -}; diff --git a/x-pack/plugins/fleet/server/services/setup.test.ts b/x-pack/plugins/fleet/server/services/setup.test.ts index 15dccb15053a..56b398c17faf 100644 --- a/x-pack/plugins/fleet/server/services/setup.test.ts +++ b/x-pack/plugins/fleet/server/services/setup.test.ts @@ -29,7 +29,6 @@ jest.mock('./setup/upgrade_package_install_version'); jest.mock('./epm/elasticsearch/template/install', () => { return { ...jest.requireActual('./epm/elasticsearch/template/install'), - ensureFileUploadWriteIndices: jest.fn(), }; }); diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 92d8f8fb37dd..d83e1424c54c 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -53,6 +53,7 @@ import { ensurePreconfiguredFleetServerHosts, getPreconfiguredFleetServerHostFromConfig, } from './preconfiguration/fleet_server_host'; +import { cleanUpOldFileIndices } from './setup/clean_old_fleet_indices'; export interface SetupStatus { isInitialized: boolean; @@ -75,6 +76,8 @@ async function createSetupSideEffects( const logger = appContextService.getLogger(); logger.info('Beginning fleet setup'); + await cleanUpOldFileIndices(esClient, logger); + await ensureFleetDirectories(); const { agentPolicies: policiesOrUndefined, packages: packagesOrUndefined } = diff --git a/x-pack/plugins/fleet/server/services/setup/clean_old_fleet_indices.test.tsx b/x-pack/plugins/fleet/server/services/setup/clean_old_fleet_indices.test.tsx new file mode 100644 index 000000000000..e98441b7043e --- /dev/null +++ b/x-pack/plugins/fleet/server/services/setup/clean_old_fleet_indices.test.tsx @@ -0,0 +1,104 @@ +/* + * 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 { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; + +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; + +import { cleanUpOldFileIndices } from './clean_old_fleet_indices'; + +describe('cleanUpOldFileIndices', () => { + it('should clean old indices and old index templates', async () => { + const logger = loggingSystemMock.createLogger(); + const esClient = elasticsearchServiceMock.createInternalClient(); + esClient.indices.get.mockResolvedValueOnce({ + '.fleet-files-agent': {}, + '.fleet-files-test': {}, + }); + esClient.indices.get.mockImplementation(({ index }) => { + if (index === '.fleet-files-agent') { + return { + '.fleet-files-agent': {}, + '.fleet-files-test': {}, + } as any; + } + return {}; + }); + + await cleanUpOldFileIndices(esClient, logger); + + expect(esClient.indices.delete).toBeCalledTimes(1); + expect(esClient.indices.delete).toBeCalledWith( + expect.objectContaining({ + index: '.fleet-files-agent,.fleet-files-test', + }) + ); + + expect(esClient.indices.deleteIndexTemplate).toBeCalledTimes(1); + expect(esClient.indices.deleteIndexTemplate).toBeCalledWith( + expect.objectContaining({ + name: '.fleet-files,.fleet-file-data,.fleet-filedelivery-data,.fleet-filedelivery-meta', + }) + ); + expect(logger.warn).not.toBeCalled(); + }); + + it('should log a warning and not throw if an unexpected error happen', async () => { + const logger = loggingSystemMock.createLogger(); + const esClient = elasticsearchServiceMock.createInternalClient(); + esClient.indices.get.mockRejectedValue(new Error('test error')); + + await cleanUpOldFileIndices(esClient, logger); + + expect(logger.warn).toBeCalledWith('Old fleet indices cleanup failed: test error'); + }); + + it('should handle 404 while deleting index template', async () => { + const logger = loggingSystemMock.createLogger(); + const esClient = elasticsearchServiceMock.createInternalClient(); + esClient.indices.get.mockResolvedValue({}); + esClient.indices.deleteIndexTemplate.mockRejectedValue({ + meta: { + statusCode: 404, + }, + }); + + await cleanUpOldFileIndices(esClient, logger); + + expect(esClient.indices.deleteIndexTemplate).toBeCalledTimes(1); + expect(logger.warn).not.toBeCalled(); + }); + + it('should handle 404 when deleting old index', async () => { + const logger = loggingSystemMock.createLogger(); + const esClient = elasticsearchServiceMock.createInternalClient(); + esClient.indices.get.mockResolvedValueOnce({ + '.fleet-files-agent': {}, + '.fleet-files-test': {}, + }); + esClient.indices.get.mockImplementation(({ index }) => { + if (index === '.fleet-files-agent') { + return { + '.fleet-files-agent': {}, + '.fleet-files-test': {}, + } as any; + } + return {}; + }); + + esClient.indices.delete.mockRejectedValue({ + meta: { + statusCode: 404, + }, + }); + + await cleanUpOldFileIndices(esClient, logger); + + expect(esClient.indices.delete).toBeCalledTimes(1); + expect(logger.warn).not.toBeCalled(); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/setup/clean_old_fleet_indices.tsx b/x-pack/plugins/fleet/server/services/setup/clean_old_fleet_indices.tsx new file mode 100644 index 000000000000..856e3543cd96 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/setup/clean_old_fleet_indices.tsx @@ -0,0 +1,67 @@ +/* + * 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 { ElasticsearchClient, Logger } from '@kbn/core/server'; +import pMap from 'p-map'; + +const INDICES_TO_CLEAN = [ + '.fleet-files-*', + '.fleet-file-data-*', + '.fleet-filedelivery-data-*', + '.fleet-filedelivery-meta-*', +]; + +const INDEX_TEMPLATE_TO_CLEAN = [ + '.fleet-files', + '.fleet-file-data', + '.fleet-filedelivery-data', + '.fleet-filedelivery-meta', +]; + +/** + * In 8.10 upload feature moved from using index to datastreams, this function allows to clean those old indices. + */ +export async function cleanUpOldFileIndices(esClient: ElasticsearchClient, logger: Logger) { + try { + // Clean indices + await pMap( + INDICES_TO_CLEAN, + async (indiceToClean) => { + const res = await esClient.indices.get({ + index: indiceToClean, + }); + const indices = Object.keys(res); + if (indices.length) { + await esClient.indices + .delete({ + index: indices.join(','), + }) + .catch((err) => { + // Skip not found errors + if (err.meta?.statusCode !== 404) { + throw err; + } + }); + } + }, + { concurrency: 2 } + ); + await esClient.indices + .deleteIndexTemplate({ + name: INDEX_TEMPLATE_TO_CLEAN.join(','), + }) + .catch((err) => { + // Skip not found errors + if (err.meta?.statusCode !== 404) { + throw err; + } + }); + // Clean index template + } catch (err) { + logger.warn(`Old fleet indices cleanup failed: ${err.message}`); + } +} diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index d9c7ff9db74a..5144d48fbd6f 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -89,6 +89,7 @@ export type { PackageListItem, PackageList, InstallationInfo, + ActionStatusOptions, } from '../../common/types'; export { ElasticsearchAssetType, KibanaAssetType, KibanaSavedObjectType } from '../../common/types'; export { dataTypes } from '../../common/constants'; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index 2ea1a2611748..e90fda99cdff 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -9,16 +9,27 @@ import { schema } from '@kbn/config-schema'; import moment from 'moment'; import semverIsValid from 'semver/functions/valid'; -import { SO_SEARCH_LIMIT } from '../../constants'; +import { SO_SEARCH_LIMIT, AGENTS_PREFIX, AGENT_MAPPINGS } from '../../constants'; import { NewAgentActionSchema } from '../models'; +import { validateKuery } from '../../routes/utils/filter_utils'; + export const GetAgentsRequestSchema = { query: schema.object( { page: schema.number({ defaultValue: 1 }), perPage: schema.number({ defaultValue: 20 }), - kuery: schema.maybe(schema.string()), + kuery: schema.maybe( + schema.string({ + validate: (value: string) => { + const validationObj = validateKuery(value, [AGENTS_PREFIX], AGENT_MAPPINGS, true); + if (validationObj?.error) { + return validationObj?.error; + } + }, + }) + ), showInactive: schema.boolean({ defaultValue: false }), withMetrics: schema.boolean({ defaultValue: false }), showUpgradeable: schema.boolean({ defaultValue: false }), @@ -206,7 +217,16 @@ export const PostBulkUpdateAgentTagsRequestSchema = { export const GetAgentStatusRequestSchema = { query: schema.object({ policyId: schema.maybe(schema.string()), - kuery: schema.maybe(schema.string()), + kuery: schema.maybe( + schema.string({ + validate: (value: string) => { + const validationObj = validateKuery(value, [AGENTS_PREFIX], AGENT_MAPPINGS, true); + if (validationObj?.error) { + return validationObj?.error; + } + }, + }) + ), }), }; @@ -221,7 +241,6 @@ export const GetActionStatusRequestSchema = { query: schema.object({ page: schema.number({ defaultValue: 0 }), perPage: schema.number({ defaultValue: 20 }), - kuery: schema.maybe(schema.string()), errorSize: schema.number({ defaultValue: 5 }), }), }; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts index 25275ed998c5..88cc6df372c1 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts @@ -9,10 +9,34 @@ import { schema } from '@kbn/config-schema'; import { NewAgentPolicySchema } from '../models'; -import { ListWithKuerySchema, BulkRequestBodySchema } from './common'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_POLICY_MAPPINGS } from '../../constants'; + +import { validateKuery } from '../../routes/utils/filter_utils'; + +import { BulkRequestBodySchema } from './common'; export const GetAgentPoliciesRequestSchema = { - query: ListWithKuerySchema.extends({ + query: schema.object({ + page: schema.maybe(schema.number({ defaultValue: 1 })), + perPage: schema.maybe(schema.number({ defaultValue: 20 })), + sortField: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])), + showUpgradeable: schema.maybe(schema.boolean()), + kuery: schema.maybe( + schema.string({ + validate: (value: string) => { + const validationObj = validateKuery( + value, + [AGENT_POLICY_SAVED_OBJECT_TYPE], + AGENT_POLICY_MAPPINGS, + true + ); + if (validationObj?.error) { + return validationObj?.error; + } + }, + }) + ), noAgentCount: schema.maybe(schema.boolean()), full: schema.maybe(schema.boolean()), }), diff --git a/x-pack/plugins/fleet/server/types/rest_spec/common.ts b/x-pack/plugins/fleet/server/types/rest_spec/common.ts index 884e5922747b..0c5f16ff87f9 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/common.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/common.ts @@ -8,9 +8,6 @@ import { schema } from '@kbn/config-schema'; import type { TypeOf } from '@kbn/config-schema'; -/** - * @deprecated - */ export const ListWithKuerySchema = schema.object({ page: schema.maybe(schema.number({ defaultValue: 1 })), perPage: schema.maybe(schema.number({ defaultValue: 20 })), diff --git a/x-pack/plugins/fleet/server/types/rest_spec/enrollment_api_key.ts b/x-pack/plugins/fleet/server/types/rest_spec/enrollment_api_key.ts index 929473699998..d7bfbb289620 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/enrollment_api_key.ts @@ -7,11 +7,31 @@ import { schema } from '@kbn/config-schema'; +import { ENROLLMENT_API_KEY_MAPPINGS } from '../../constants'; + +import { FLEET_ENROLLMENT_API_PREFIX } from '../../../common/constants'; + +import { validateKuery } from '../../routes/utils/filter_utils'; + export const GetEnrollmentAPIKeysRequestSchema = { query: schema.object({ page: schema.number({ defaultValue: 1 }), perPage: schema.number({ defaultValue: 20 }), - kuery: schema.maybe(schema.string()), + kuery: schema.maybe( + schema.string({ + validate: (value: string) => { + const validationObj = validateKuery( + value, + [FLEET_ENROLLMENT_API_PREFIX], + ENROLLMENT_API_KEY_MAPPINGS, + true + ); + if (validationObj?.error) { + return validationObj?.error; + } + }, + }) + ), }), }; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts b/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts index 1538f380fb7b..88b4452a5fe7 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts @@ -15,10 +15,34 @@ import { import { inputsFormat } from '../../../common/constants'; -import { ListWithKuerySchema, BulkRequestBodySchema } from './common'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICIES_MAPPINGS } from '../../constants'; + +import { validateKuery } from '../../routes/utils/filter_utils'; + +import { BulkRequestBodySchema } from './common'; export const GetPackagePoliciesRequestSchema = { - query: ListWithKuerySchema.extends({ + query: schema.object({ + page: schema.maybe(schema.number({ defaultValue: 1 })), + perPage: schema.maybe(schema.number({ defaultValue: 20 })), + sortField: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])), + showUpgradeable: schema.maybe(schema.boolean()), + kuery: schema.maybe( + schema.string({ + validate: (value: string) => { + const validationObj = validateKuery( + value, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS, + true + ); + if (validationObj?.error) { + return validationObj?.error; + } + }, + }) + ), format: schema.maybe( schema.oneOf([schema.literal(inputsFormat.Simplified), schema.literal(inputsFormat.Legacy)]) ), diff --git a/x-pack/plugins/fleet/server/types/rest_spec/tags.ts b/x-pack/plugins/fleet/server/types/rest_spec/tags.ts index 2b454647296e..beb646ccec9d 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/tags.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/tags.ts @@ -7,9 +7,22 @@ import { schema } from '@kbn/config-schema'; +import { validateKuery } from '../../routes/utils/filter_utils'; + +import { AGENTS_PREFIX, AGENT_MAPPINGS } from '../../constants'; + export const GetTagsRequestSchema = { query: schema.object({ - kuery: schema.maybe(schema.string()), + kuery: schema.maybe( + schema.string({ + validate: (value: string) => { + const validationObj = validateKuery(value, [AGENTS_PREFIX], AGENT_MAPPINGS, true); + if (validationObj?.error) { + return validationObj?.error; + } + }, + }) + ), showInactive: schema.boolean({ defaultValue: false }), }), }; diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index 4e8ec7de3fcc..82b22c90779b 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -99,5 +99,6 @@ "@kbn/shared-ux-file-types", "@kbn/core-http-router-server-mocks", "@kbn/core-application-browser", + "@kbn/core-saved-objects-base-server-internal", ] } diff --git a/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts b/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts index ff90e1d40d30..ac1cf6ef1f94 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts @@ -326,4 +326,65 @@ describe('format_column', () => { }, }); }); + + it('does translate the duration params into native parameters', async () => { + const result = await fn(datatable, { + columnId: 'test', + format: 'duration', + fromUnit: 'seconds', + toUnit: 'asHours', + compact: true, + decimals: 2, + }); + + expect(result.columns[0].meta).toEqual({ + type: 'number', + params: { + id: 'duration', + params: { + pattern: '', + formatOverride: true, + inputFormat: 'seconds', + outputFormat: 'asHours', + outputPrecision: 2, + useShortSuffix: true, + showSuffix: true, + includeSpaceWithSuffix: true, + }, + }, + }); + }); + + it('should apply custom suffix to duration format when configured', async () => { + const result = await fn(datatable, { + columnId: 'test', + format: 'duration', + fromUnit: 'seconds', + toUnit: 'asHours', + compact: true, + decimals: 2, + suffix: ' on Earth', + }); + expect(result.columns[0].meta).toEqual({ + type: 'number', + params: { + id: 'suffix', + params: { + suffixString: ' on Earth', + id: 'duration', + formatOverride: true, + params: { + pattern: '', + formatOverride: true, + inputFormat: 'seconds', + outputFormat: 'asHours', + outputPrecision: 2, + useShortSuffix: true, + showSuffix: true, + includeSpaceWithSuffix: true, + }, + }, + }, + }); + }); }); diff --git a/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts b/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts index 84e83aa2a9d1..e99aaa757702 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts @@ -42,7 +42,16 @@ function getPatternFromFormat( export const formatColumnFn: FormatColumnExpressionFunction['fn'] = ( input, - { format, columnId, decimals, compact, suffix, pattern, parentFormat }: FormatColumnArgs + { + format, + columnId, + decimals, + compact, + suffix, + pattern, + parentFormat, + ...otherArgs + }: FormatColumnArgs ) => ({ ...input, columns: input.columns @@ -56,6 +65,12 @@ export const formatColumnFn: FormatColumnExpressionFunction['fn'] = ( params: { pattern: getPatternFromFormat(format, decimals, compact, pattern), formatOverride: true, + ...supportedFormats[format].translateToFormatParams?.({ + decimals, + compact, + suffix, + ...otherArgs, + }), }, }; return withParams(col, serializedFormat as Record); @@ -80,6 +95,12 @@ export const formatColumnFn: FormatColumnExpressionFunction['fn'] = ( const customParams = { pattern: getPatternFromFormat(format, decimals, compact, pattern), formatOverride: true, + ...supportedFormats[format].translateToFormatParams?.({ + decimals, + compact, + suffix, + ...otherArgs, + }), }; // Some parent formatters are multi-fields and wrap the custom format into a "paramsPerField" // property. Here the format is passed to this property to make it work properly diff --git a/x-pack/plugins/lens/common/expressions/format_column/index.ts b/x-pack/plugins/lens/common/expressions/format_column/index.ts index 7acbe3237c0e..7cfb5e8afed9 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/index.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/index.ts @@ -15,6 +15,8 @@ export interface FormatColumnArgs { compact?: boolean; pattern?: string; parentFormat?: string; + fromUnit?: string; + toUnit?: string; } export const formatColumn: FormatColumnExpressionFunction = { @@ -52,6 +54,14 @@ export const formatColumn: FormatColumnExpressionFunction = { types: ['string'], help: '', }, + fromUnit: { + types: ['string'], + help: '', + }, + toUnit: { + types: ['string'], + help: '', + }, }, inputTypes: ['datatable'], async fn(...args) { diff --git a/x-pack/plugins/lens/common/expressions/format_column/supported_formats.ts b/x-pack/plugins/lens/common/expressions/format_column/supported_formats.ts index 9c1ef439bad3..b13b9348577d 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/supported_formats.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/supported_formats.ts @@ -5,9 +5,21 @@ * 2.0. */ +import { + DEFAULT_DURATION_INPUT_FORMAT, + DEFAULT_DURATION_OUTPUT_FORMAT, +} from '@kbn/field-formats-plugin/common'; +import type { FormatColumnArgs } from '.'; + export const supportedFormats: Record< string, - { decimalsToPattern: (decimals?: number, compact?: boolean) => string; formatId: string } + { + formatId: string; + decimalsToPattern: (decimals?: number, compact?: boolean) => string; + translateToFormatParams?: ( + params: Omit + ) => Record; + } > = { number: { formatId: 'number', @@ -45,6 +57,20 @@ export const supportedFormats: Record< return `0,0.${'0'.repeat(decimals)}bitd`; }, }, + duration: { + formatId: 'duration', + decimalsToPattern: () => '', + translateToFormatParams: (params) => { + return { + inputFormat: params.fromUnit || DEFAULT_DURATION_INPUT_FORMAT.kind, + outputFormat: params.toUnit || DEFAULT_DURATION_OUTPUT_FORMAT.method, + outputPrecision: params.decimals, + useShortSuffix: Boolean(params.compact), + showSuffix: true, + includeSpaceWithSuffix: true, + }; + }, + }, custom: { formatId: 'custom', decimalsToPattern: () => '', diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.test.tsx index 81d6040abefd..af45608665b0 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.test.tsx @@ -14,7 +14,7 @@ import { LensAppServices } from '../../../app_plugin/types'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { I18nProvider } from '@kbn/i18n-react'; import { coreMock } from '@kbn/core/public/mocks'; -import { EuiFieldNumber } from '@elastic/eui'; +import { EuiComboBox, EuiFieldNumber } from '@elastic/eui'; jest.mock('lodash', () => { const original = jest.requireActual('lodash'); @@ -106,10 +106,10 @@ describe('FormatSelector', () => { }); expect(props.onChange).toBeCalledWith({ id: 'bytes', params: { decimals: 0 } }); }); - it('updates the suffix', async () => { + it('updates the suffix', () => { const props = getDefaultProps(); const component = mountWithServices(); - await act(async () => { + act(() => { component .find('[data-test-subj="indexPattern-dimension-formatSuffix"]') .last() @@ -120,4 +120,55 @@ describe('FormatSelector', () => { component.update(); expect(props.onChange).toBeCalledWith({ id: 'bytes', params: { suffix: 'GB' } }); }); + + describe('Duration', () => { + it('disables the decimals and compact controls for humanize approximate output', () => { + const originalProps = getDefaultProps(); + let component = mountWithServices( + + ); + + expect( + component + .find('[data-test-subj="indexPattern-dimension-formatDecimals"]') + .last() + .prop('disabled') + ).toBe(true); + expect( + component + .find('[data-test-subj="lns-indexpattern-dimension-formatCompact"]') + .first() + .prop('disabled') + ).toBe(true); + + act(() => { + component + .find('[data-test-subj="indexPattern-dimension-duration-end"]') + .find(EuiComboBox) + .prop('onChange')!([{ label: 'Hours', value: 'asHours' }]); + }); + component = component.update(); + + expect( + component + .find('[data-test-subj="indexPattern-dimension-formatDecimals"]') + .last() + .prop('disabled') + ).toBe(false); + expect( + component + .find('[data-test-subj="lns-indexpattern-dimension-formatCompact"]') + .first() + .prop('disabled') + ).toBe(false); + }); + }); }); diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.tsx index af0645a0c4c2..e233cb3cf7f2 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/format_selector.tsx @@ -16,35 +16,52 @@ import { EuiSwitch, EuiCode, } from '@elastic/eui'; -import { useDebouncedValue } from '@kbn/visualization-ui-components'; +import { useDebouncedValue, TooltipWrapper } from '@kbn/visualization-ui-components'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { FORMATS_UI_SETTINGS } from '@kbn/field-formats-plugin/common'; +import { + DEFAULT_DURATION_INPUT_FORMAT, + DEFAULT_DURATION_OUTPUT_FORMAT, + FORMATS_UI_SETTINGS, +} from '@kbn/field-formats-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; import { LensAppServices } from '../../../app_plugin/types'; import { GenericIndexPatternColumn } from '../form_based'; import { isColumnFormatted } from '../operations/definitions/helpers'; import { ValueFormatConfig } from '../operations/definitions/column_types'; +import { DurationRowInputs } from './formatting/duration_input'; const supportedFormats: Record< string, - { title: string; defaultDecimals?: number; supportsCompact: boolean } + { + title: string; + defaultDecimals?: number; + supportsCompact: boolean; + supportsDecimals: boolean; + supportsSuffix: boolean; + } > = { number: { title: i18n.translate('xpack.lens.indexPattern.numberFormatLabel', { defaultMessage: 'Number', }), + supportsDecimals: true, + supportsSuffix: true, supportsCompact: true, }, percent: { title: i18n.translate('xpack.lens.indexPattern.percentFormatLabel', { defaultMessage: 'Percent', }), + supportsDecimals: true, + supportsSuffix: true, supportsCompact: true, }, bytes: { title: i18n.translate('xpack.lens.indexPattern.bytesFormatLabel', { defaultMessage: 'Bytes (1024)', }), + supportsDecimals: true, + supportsSuffix: true, supportsCompact: false, }, bits: { @@ -52,13 +69,26 @@ const supportedFormats: Record< defaultMessage: 'Bits (1000)', }), defaultDecimals: 0, + supportsDecimals: true, + supportsSuffix: true, supportsCompact: false, }, + duration: { + title: i18n.translate('xpack.lens.indexPattern.durationLabel', { + defaultMessage: 'Duration', + }), + defaultDecimals: 0, + supportsDecimals: true, + supportsSuffix: true, + supportsCompact: true, + }, custom: { title: i18n.translate('xpack.lens.indexPattern.customFormatLabel', { defaultMessage: 'Custom format', }), defaultDecimals: 0, + supportsDecimals: false, + supportsSuffix: false, supportsCompact: false, }, }; @@ -164,6 +194,20 @@ export function FormatSelector(props: FormatSelectorProps) { onChange ); + const { setter: setDurationFrom, value: durationFrom } = useDebouncedInputforParam( + 'fromUnit' as const, + DEFAULT_DURATION_INPUT_FORMAT.kind, + currentFormat, + onChange + ); + + const { setter: setDurationTo, value: durationTo } = useDebouncedInputforParam( + 'toUnit' as const, + DEFAULT_DURATION_OUTPUT_FORMAT.method, + currentFormat, + onChange + ); + const selectedFormat = currentFormat?.id ? supportedFormats[currentFormat.id] : undefined; const stableOptions = useMemo( () => [ @@ -210,6 +254,8 @@ export function FormatSelector(props: FormatSelectorProps) { [currentFormat, selectedFormat?.title] ); + const approximatedFormat = currentFormat?.id === 'duration' && durationTo === 'humanize'; + return ( <> - {currentFormat && currentFormat.id !== 'custom' ? ( - <> - - { - const value = Number(e.currentTarget.value); - setDecimals(value); - const validatedValue = Math.min(RANGE_MAX, Math.max(RANGE_MIN, value)); - onChange({ - id: currentFormat.id, - params: { - ...currentFormat.params, - decimals: validatedValue, - }, - }); - }} - data-test-subj="indexPattern-dimension-formatDecimals" - compressed - fullWidth - prepend={decimalsLabel} - aria-label={decimalsLabel} - /> - - - { - setSuffix(e.currentTarget.value); - }} - data-test-subj="indexPattern-dimension-formatSuffix" - compressed - fullWidth - prepend={suffixLabel} - aria-label={suffixLabel} - /> - - ) : null} - {selectedFormat?.supportsCompact ? ( + {currentFormat && selectedFormat ? ( <> - - setCompact(!compact)} - data-test-subj="lns-indexpattern-dimension-formatCompact" - /> + {currentFormat?.id === 'duration' ? ( + <> + + + + ) : null} + {selectedFormat.supportsDecimals ? ( + <> + + + { + const value = Number(e.currentTarget.value); + setDecimals(value); + const validatedValue = Math.min(RANGE_MAX, Math.max(RANGE_MIN, value)); + onChange({ + id: currentFormat.id, + params: { + ...currentFormat.params, + decimals: validatedValue, + }, + }); + }} + data-test-subj="indexPattern-dimension-formatDecimals" + compressed + fullWidth + prepend={decimalsLabel} + aria-label={decimalsLabel} + disabled={approximatedFormat} + /> + + + ) : null} + {selectedFormat.supportsSuffix ? ( + <> + + { + setSuffix(e.currentTarget.value); + }} + data-test-subj="indexPattern-dimension-formatSuffix" + compressed + fullWidth + prepend={suffixLabel} + aria-label={suffixLabel} + /> + + ) : null} + {selectedFormat.supportsCompact ? ( + <> + + + setCompact(!compact)} + data-test-subj="lns-indexpattern-dimension-formatCompact" + disabled={approximatedFormat} + /> + + + ) : null} ) : null}
diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/formatting/duration_input.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/formatting/duration_input.tsx new file mode 100644 index 000000000000..e5414af51f45 --- /dev/null +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/formatting/duration_input.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiComboBox, EuiSpacer } from '@elastic/eui'; +import { DURATION_INPUT_FORMATS, DURATION_OUTPUT_FORMATS } from '@kbn/field-formats-plugin/common'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export const durationOutputOptions = DURATION_OUTPUT_FORMATS.map(({ text, method }) => ({ + label: text, + value: method, +})); +export const durationInputOptions = DURATION_INPUT_FORMATS.map(({ text, kind }) => ({ + label: text, + value: kind, +})); + +interface DurationInputProps { + testSubjLayout?: string; + testSubjStart?: string; + testSubjEnd?: string; + onStartChange: (newStartValue: string) => void; + onEndChange: (newEndValue: string) => void; + startValue: string | undefined; + endValue: string | undefined; +} + +function getSelectedOption( + inputValue: string, + list: Array<{ label: string; value: string }> +): Array<{ label: string; value: string }> { + const option = list.find(({ value }) => inputValue === value); + return option ? [option] : []; +} + +export const DurationRowInputs = ({ + testSubjLayout, + testSubjStart, + testSubjEnd, + startValue = 'milliseconds', + endValue = 'seconds', + onStartChange, + onEndChange, +}: DurationInputProps) => { + return ( + <> + onStartChange(newStartValue.value!)} + singleSelection={{ asPlainText: true }} + data-test-subj={testSubjStart} + compressed + /> + + onEndChange(newEndChange.value!)} + singleSelection={{ asPlainText: true }} + data-test-subj={testSubjEnd} + compressed + /> + + ); +}; diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/column_types.ts b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/column_types.ts index e10955b3c54b..971f952c73dc 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/column_types.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/column_types.ts @@ -27,6 +27,8 @@ export interface ValueFormatConfig { suffix?: string; compact?: boolean; pattern?: string; + fromUnit?: string; + toUnit?: string; }; } diff --git a/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts b/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts index d28df1c3e5f9..33bec4c23a1b 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts @@ -27,7 +27,7 @@ import { GenericIndexPatternColumn } from './form_based'; import { operationDefinitionMap } from './operations'; import { FormBasedPrivateState, FormBasedLayer } from './types'; import { DateHistogramIndexPatternColumn, RangeIndexPatternColumn } from './operations/definitions'; -import { FormattedIndexPatternColumn } from './operations/definitions/column_types'; +import type { FormattedIndexPatternColumn } from './operations/definitions/column_types'; import { isColumnFormatted, isColumnOfType } from './operations/definitions/helpers'; import type { IndexPattern, IndexPatternMap } from '../../types'; import { dedupeAggs } from './dedupe_aggs'; @@ -352,6 +352,14 @@ function getExpressionForLayer( format?.params && 'pattern' in format.params && format.params.pattern ? [format.params.pattern] : [], + fromUnit: + format?.params && 'fromUnit' in format.params && format.params.fromUnit + ? [format.params.fromUnit] + : [], + toUnit: + format?.params && 'toUnit' in format.params && format.params.toUnit + ? [format.params.toUnit] + : [], parentFormat: parentFormat ? [JSON.stringify(parentFormat)] : [], }, }; diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 2ce87e8d3d8e..b3e11a70839c 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -303,9 +303,9 @@ export const ConfigSchema = schema.object({ schema.contextRef('serverless'), true, schema.object({ - userManagementEnabled: schema.boolean({ defaultValue: false }), - roleManagementEnabled: schema.boolean({ defaultValue: false }), - roleMappingManagementEnabled: schema.boolean({ defaultValue: false }), + userManagementEnabled: schema.boolean({ defaultValue: true }), + roleManagementEnabled: schema.boolean({ defaultValue: true }), + roleMappingManagementEnabled: schema.boolean({ defaultValue: true }), }), schema.never() ), diff --git a/x-pack/plugins/security_solution/common/utils/get_ramdom_color.ts b/x-pack/plugins/security_solution/common/utils/get_ramdom_color.ts new file mode 100644 index 000000000000..67ff5686f98d --- /dev/null +++ b/x-pack/plugins/security_solution/common/utils/get_ramdom_color.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Returns the hex representation of a random color (e.g `#F1B7E2`) + */ +export const getRandomColor = (): string => { + return `#${String(Math.floor(Math.random() * 16777215).toString(16)).padStart(6, '0')}`; +}; diff --git a/x-pack/plugins/security_solution/jest.config.dev.js b/x-pack/plugins/security_solution/jest.config.dev.js index 1810adc42e36..1aaace56c5f8 100644 --- a/x-pack/plugins/security_solution/jest.config.dev.js +++ b/x-pack/plugins/security_solution/jest.config.dev.js @@ -12,5 +12,6 @@ module.exports = { '/x-pack/plugins/security_solution/common/*/jest.config.js', '/x-pack/plugins/security_solution/server/*/jest.config.js', '/x-pack/plugins/security_solution/public/*/jest.config.js', + '/x-pack/plugins/security_solution/scripts/junit_transformer/*/jest.config.js', ], }; diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 01419bb09e47..a56441615417 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -25,9 +25,10 @@ "cypress:dw:endpoint:open": "node ./scripts/start_cypress_parallel open --config-file ./public/management/cypress_endpoint.config.ts ts --ftr-config-file ../../../../../../x-pack/test/defend_workflows_cypress/endpoint_config", "cypress:investigations:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/e2e/investigations/**/*.cy.ts' --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config; status=$?; yarn junit:merge && exit $status", "cypress:explore:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/e2e/explore/**/*.cy.ts' --ftr-config-file ../../../../../../x-pack/test/security_solution_cypress/cli_config; status=$?; yarn junit:merge && exit $status", - "junit:merge": "../../../node_modules/.bin/mochawesome-merge ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../target/kibana-security-solution/cypress/results/output.json && ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results && mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/", + "junit:merge": "../../../node_modules/.bin/mochawesome-merge ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../target/kibana-security-solution/cypress/results/output.json && ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results && yarn junit:transform && mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/", "test:generate": "node scripts/endpoint/resolver_generator", "mappings:generate": "node scripts/mappings/mappings_generator", - "mappings:load": "node scripts/mappings/mappings_loader" + "mappings:load": "node scripts/mappings/mappings_loader", + "junit:transform": "node scripts/junit_transformer --pathPattern '../../../target/kibana-security-solution/cypress/results/*.xml' --rootDirectory ../../../ --reportName 'Security Solution Cypress' --writeInPlace" } } diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx index f8859706bd43..9643efcb8b0b 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx @@ -11,8 +11,7 @@ import { EuiThemeProvider, useEuiTheme } from '@elastic/eui'; import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template'; -import { ExpandableFlyout, ExpandableFlyoutProvider } from '@kbn/expandable-flyout'; -import { expandableFlyoutDocumentsPanels } from '../../../flyout'; +import { SecuritySolutionFlyout, SecuritySolutionFlyoutContextProvider } from '../../../flyout'; import { useSecuritySolutionNavigation } from '../../../common/components/navigation/use_security_solution_navigation'; import { TimelineId } from '../../../../common/types/timeline'; import { getTimelineShowStatusByIdSelector } from '../../../timelines/components/flyout/selectors'; @@ -24,7 +23,6 @@ import { SecuritySolutionBottomBarProps, } from './bottom_bar'; import { useShowTimeline } from '../../../common/utils/timeline/use_show_timeline'; -import { useSyncFlyoutStateWithUrl } from '../../../flyout/url/use_sync_flyout_state_with_url'; /** * Need to apply the styles via a className to effect the containing bottom bar @@ -69,8 +67,6 @@ export const SecuritySolutionTemplateWrapper: React.FC + )} - + - + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/alert_count_by_status/mock_data.ts b/x-pack/plugins/security_solution/public/common/components/alert_count_by_status/mock_data.ts index 85cd90cc5226..a0320edf2842 100644 --- a/x-pack/plugins/security_solution/public/common/components/alert_count_by_status/mock_data.ts +++ b/x-pack/plugins/security_solution/public/common/components/alert_count_by_status/mock_data.ts @@ -19,8 +19,8 @@ export const mockAlertCountByRuleResult = { hits: { hits: [ { - _source: { - 'kibana.alert.rule.uuid': '100', + fields: { + 'kibana.alert.rule.uuid': ['100'], }, }, ], @@ -34,8 +34,8 @@ export const mockAlertCountByRuleResult = { hits: { hits: [ { - _source: { - 'kibana.alert.rule.uuid': '200', + fields: { + 'kibana.alert.rule.uuid': ['200'], }, }, ], @@ -49,8 +49,8 @@ export const mockAlertCountByRuleResult = { hits: { hits: [ { - _source: { - 'kibana.alert.rule.uuid': '300', + fields: { + 'kibana.alert.rule.uuid': ['300'], }, }, ], @@ -64,8 +64,8 @@ export const mockAlertCountByRuleResult = { hits: { hits: [ { - _source: { - 'kibana.alert.rule.uuid': '400', + fields: { + 'kibana.alert.rule.uuid': ['400'], }, }, ], @@ -79,8 +79,8 @@ export const mockAlertCountByRuleResult = { hits: { hits: [ { - _source: { - 'kibana.alert.rule.uuid': '500', + fields: { + 'kibana.alert.rule.uuid': ['500'], }, }, ], @@ -94,8 +94,8 @@ export const mockAlertCountByRuleResult = { hits: { hits: [ { - _source: { - 'kibana.alert.rule.uuid': '600', + fields: { + 'kibana.alert.rule.uuid': ['600'], }, }, ], @@ -109,8 +109,8 @@ export const mockAlertCountByRuleResult = { hits: { hits: [ { - _source: { - 'kibana.alert.rule.uuid': '700', + fields: { + 'kibana.alert.rule.uuid': ['700'], }, }, ], diff --git a/x-pack/plugins/security_solution/public/common/components/alert_count_by_status/use_alert_count_by_rule_by_status.ts b/x-pack/plugins/security_solution/public/common/components/alert_count_by_status/use_alert_count_by_rule_by_status.ts index c49fdb19310a..9c1b3ae525ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/alert_count_by_status/use_alert_count_by_rule_by_status.ts +++ b/x-pack/plugins/security_solution/public/common/components/alert_count_by_status/use_alert_count_by_rule_by_status.ts @@ -117,6 +117,8 @@ export const useAlertCountByRuleByStatus: UseAlertCountByRuleByStatus = ({ return { items, isLoading, updatedAt }; }; +export const KIBANA_RULE_ID = 'kibana.alert.rule.uuid'; + export const buildRuleAlertsByEntityQuery = ({ additionalFilters = [], from, @@ -133,6 +135,8 @@ export const buildRuleAlertsByEntityQuery = ({ value: string; }) => ({ size: 0, + _source: false, + fields: [KIBANA_RULE_ID], query: { bool: { filter: [ @@ -145,11 +149,15 @@ export const buildRuleAlertsByEntityQuery = ({ }, }, }, - { - terms: { - 'kibana.alert.workflow_status': statuses, - }, - }, + ...(statuses?.length > 0 + ? [ + { + terms: { + 'kibana.alert.workflow_status': statuses, + }, + }, + ] + : []), { term: { [field]: value, @@ -167,7 +175,8 @@ export const buildRuleAlertsByEntityQuery = ({ aggs: { ruleUuid: { top_hits: { - _source: ['kibana.alert.rule.uuid'], + _source: false, + fields: [KIBANA_RULE_ID], size: 1, }, }, @@ -181,8 +190,8 @@ interface RuleUuidData extends GenericBuckets { hits: { hits: [ { - _source: { - 'kibana.alert.rule.uuid': string; + fields: { + 'kibana.alert.rule.uuid': string[]; }; } ]; @@ -201,7 +210,8 @@ const parseAlertCountByRuleItems = ( ): AlertCountByRuleByStatusItem[] => { const buckets = aggregations?.[ALERTS_BY_RULE_AGG].buckets ?? []; return buckets.map((bucket) => { - const uuid = bucket.ruleUuid.hits?.hits[0]?._source['kibana.alert.rule.uuid'] || ''; + const uuid = + firstNonNullValue(bucket.ruleUuid.hits?.hits[0]?.fields['kibana.alert.rule.uuid']) ?? ''; return { ruleName: firstNonNullValue(bucket.key) ?? '-', count: bucket.doc_count, diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts index 9a5314d0a0d0..d21e7a8087cb 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts @@ -125,7 +125,7 @@ function formatResultData( ): AnomaliesCount[] { const unsortedAnomalies: AnomaliesCount[] = anomaliesJobs.map((job) => { const bucket = buckets.find(({ key }) => key === job?.id); - const hasUserName = has("entity.hits.hits[0]._source['user.name']", bucket); + const hasUserName = has("entity.hits.hits[0].fields['user.name']", bucket); return { name: job?.customSettings?.security_app_display_name ?? job.id, diff --git a/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts b/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts index 6f87ede894dd..d7401ff19d91 100644 --- a/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts @@ -6,11 +6,23 @@ */ export const MOCK_TAG_ID = 'securityTagId'; +export const MOCK_TAG_NAME = 'test tag'; export const DEFAULT_TAGS_RESPONSE = [ { id: MOCK_TAG_ID, - name: 'test tag', + attributes: { + name: MOCK_TAG_NAME, + description: 'test tag description', + color: '#2c7b82', + }, + }, +]; + +export const DEFAULT_CREATE_TAGS_RESPONSE = [ + { + id: MOCK_TAG_ID, + name: MOCK_TAG_NAME, description: 'test tag description', color: '#2c7b82', }, @@ -21,4 +33,4 @@ export const getTagsByName = jest .mockImplementation(() => Promise.resolve(DEFAULT_TAGS_RESPONSE)); export const createTag = jest .fn() - .mockImplementation(() => Promise.resolve(DEFAULT_TAGS_RESPONSE[0])); + .mockImplementation(() => Promise.resolve(DEFAULT_CREATE_TAGS_RESPONSE[0])); diff --git a/x-pack/plugins/security_solution/public/common/containers/tags/api.ts b/x-pack/plugins/security_solution/public/common/containers/tags/api.ts index 57b27318103f..479ae07cc4eb 100644 --- a/x-pack/plugins/security_solution/public/common/containers/tags/api.ts +++ b/x-pack/plugins/security_solution/public/common/containers/tags/api.ts @@ -6,20 +6,29 @@ */ import type { HttpSetup } from '@kbn/core/public'; -import type { Tag } from '@kbn/saved-objects-tagging-plugin/public'; -import type { TagAttributes } from '@kbn/saved-objects-tagging-plugin/common'; +import type { + ITagsClient, + TagAttributes, + Tag as TagResponse, +} from '@kbn/saved-objects-tagging-plugin/common'; import { INTERNAL_TAGS_URL } from '../../../../common/constants'; +export interface Tag { + id: string; + attributes: TagAttributes; +} + export const getTagsByName = ( { http, tagName }: { http: HttpSetup; tagName: string }, abortSignal?: AbortSignal ): Promise => http.get(INTERNAL_TAGS_URL, { query: { name: tagName }, signal: abortSignal }); -export const createTag = ( - { http, tag }: { http: HttpSetup; tag: Omit & { color?: string } }, - abortSignal?: AbortSignal -): Promise => - http.put(INTERNAL_TAGS_URL, { - body: JSON.stringify(tag), - signal: abortSignal, - }); +// Dashboard listing needs savedObjectsTaggingClient to work correctly with cache. +// https://github.com/elastic/kibana/issues/160723#issuecomment-1641904984 +export const createTag = ({ + savedObjectsTaggingClient, + tag, +}: { + savedObjectsTaggingClient: ITagsClient; + tag: TagAttributes; +}): Promise => savedObjectsTaggingClient.create(tag); diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/eql_search_response.mock.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/eql_search_response.mock.ts deleted file mode 100644 index 833fbf22a725..000000000000 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/eql_search_response.mock.ts +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { EqlSearchStrategyResponse } from '@kbn/data-plugin/common'; -import type { Source } from './types'; -import type { EqlSearchResponse } from '../../../../common/detection_engine/types'; -import type { Connection } from '@elastic/elasticsearch'; - -export const getMockEqlResponse = (): EqlSearchStrategyResponse> => ({ - id: 'some-id', - rawResponse: { - body: { - hits: { - events: [ - { - _index: 'index', - _id: '1', - _source: { - '@timestamp': '2020-10-04T15:16:54.368707900Z', - }, - }, - { - _index: 'index', - _id: '2', - _source: { - '@timestamp': '2020-10-04T15:50:54.368707900Z', - }, - }, - { - _index: 'index', - _id: '3', - _source: { - '@timestamp': '2020-10-04T15:06:54.368707900Z', - }, - }, - { - _index: 'index', - _id: '4', - _source: { - '@timestamp': '2020-10-04T15:15:54.368707900Z', - }, - }, - ], - total: { - value: 4, - relation: '', - }, - }, - is_partial: false, - is_running: false, - took: 300, - timed_out: false, - }, - headers: {}, - warnings: [], - meta: { - aborted: false, - attempts: 0, - context: null, - name: 'elasticsearch-js', - connection: {} as Connection, - request: { - params: { - body: JSON.stringify({ - filter: { - range: { - '@timestamp': { - gte: '2020-10-07T00:46:12.414Z', - lte: '2020-10-07T01:46:12.414Z', - format: 'strict_date_optional_time', - }, - }, - }, - }), - method: 'GET', - path: '/_eql/search/', - querystring: 'some query string', - }, - options: {}, - id: '', - }, - }, - statusCode: 200, - }, -}); - -export const getMockEndgameEqlResponse = (): EqlSearchStrategyResponse< - EqlSearchResponse -> => ({ - id: 'some-id', - rawResponse: { - body: { - hits: { - events: [ - { - _index: 'index', - _id: '1', - _source: { - '@timestamp': 1601824614000, - }, - }, - { - _index: 'index', - _id: '2', - _source: { - '@timestamp': 1601826654368, - }, - }, - { - _index: 'index', - _id: '3', - _source: { - '@timestamp': 1601824014368, - }, - }, - { - _index: 'index', - _id: '4', - _source: { - '@timestamp': 1601824554368, - }, - }, - ], - total: { - value: 4, - relation: '', - }, - }, - is_partial: false, - is_running: false, - took: 300, - timed_out: false, - }, - headers: {}, - warnings: [], - meta: { - aborted: false, - attempts: 0, - context: null, - name: 'elasticsearch-js', - connection: {} as Connection, - request: { - params: { - body: JSON.stringify({ - filter: { - range: { - '@timestamp': { - gte: '2020-10-07T00:46:12.414Z', - lte: '2020-10-07T01:46:12.414Z', - format: 'strict_date_optional_time', - }, - }, - }, - }), - method: 'GET', - path: '/_eql/search/', - querystring: 'some query string', - }, - options: {}, - id: '', - }, - }, - statusCode: 200, - }, -}); - -export const getMockEqlSequenceResponse = (): EqlSearchStrategyResponse< - EqlSearchResponse -> => ({ - id: 'some-id', - rawResponse: { - body: { - hits: { - sequences: [ - { - join_keys: [], - events: [ - { - _index: 'index', - _id: '1', - _source: { - '@timestamp': '2020-10-04T15:16:54.368707900Z', - }, - }, - { - _index: 'index', - _id: '2', - _source: { - '@timestamp': '2020-10-04T15:50:54.368707900Z', - }, - }, - ], - }, - { - join_keys: [], - events: [ - { - _index: 'index', - _id: '3', - _source: { - '@timestamp': '2020-10-04T15:06:54.368707900Z', - }, - }, - { - _index: 'index', - _id: '4', - _source: { - '@timestamp': '2020-10-04T15:15:54.368707900Z', - }, - }, - ], - }, - ], - total: { - value: 4, - relation: '', - }, - }, - is_partial: false, - is_running: false, - took: 300, - timed_out: false, - }, - headers: {}, - warnings: [], - meta: { - aborted: false, - attempts: 0, - context: null, - name: 'elasticsearch-js', - connection: {} as Connection, - request: { - params: { - body: JSON.stringify({ - filter: { - range: { - '@timestamp': { - gte: '2020-10-07T00:46:12.414Z', - lte: '2020-10-07T01:46:12.414Z', - format: 'strict_date_optional_time', - }, - }, - }, - }), - method: 'GET', - path: '/_eql/search/', - querystring: 'some query string', - }, - options: {}, - id: '', - }, - }, - statusCode: 200, - }, -}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts deleted file mode 100644 index a93a71f8382a..000000000000 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts +++ /dev/null @@ -1,781 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; - -import type { EqlSearchStrategyResponse } from '@kbn/data-plugin/common'; -import type { Source } from './types'; -import type { EqlSearchResponse } from '../../../../common/detection_engine/types'; -import type { inputsModel } from '../../store'; - -import { - calculateBucketForHour, - calculateBucketForDay, - getEqlAggsData, - createIntervalArray, - getInterval, - formatInspect, - getEventsToBucket, -} from './helpers'; -import { - getMockEndgameEqlResponse, - getMockEqlResponse, - getMockEqlSequenceResponse, -} from './eql_search_response.mock'; - -describe('eql/helpers', () => { - describe('calculateBucketForHour', () => { - test('returns 2 if the difference in times is 2 minutes', () => { - const diff = calculateBucketForHour( - Date.parse('2020-02-20T05:56:54.037Z'), - Date.parse('2020-02-20T05:57:54.037Z') - ); - - expect(diff).toEqual(2); - }); - - test('returns 10 if the difference in times is 8-10 minutes', () => { - const diff = calculateBucketForHour( - Date.parse('2020-02-20T05:48:54.037Z'), - Date.parse('2020-02-20T05:57:54.037Z') - ); - - expect(diff).toEqual(10); - }); - - test('returns 16 if the difference in times is 10-15 minutes', () => { - const diff = calculateBucketForHour( - Date.parse('2020-02-20T05:42:54.037Z'), - Date.parse('2020-02-20T05:57:54.037Z') - ); - - expect(diff).toEqual(16); - }); - - test('returns 60 if the difference in times is 58-60 minutes', () => { - const diff = calculateBucketForHour( - Date.parse('2020-02-20T04:58:54.037Z'), - Date.parse('2020-02-20T05:57:54.037Z') - ); - - expect(diff).toEqual(60); - }); - - test('returns exact time difference if it is a multiple of 2', () => { - const diff = calculateBucketForHour( - Date.parse('2020-02-20T05:37:54.037Z'), - Date.parse('2020-02-20T05:57:54.037Z') - ); - - expect(diff).toEqual(20); - }); - - test('returns 0 if times are equal', () => { - const diff = calculateBucketForHour( - Date.parse('2020-02-20T05:57:54.037Z'), - Date.parse('2020-02-20T05:57:54.037Z') - ); - - expect(diff).toEqual(0); - }); - - test('returns 2 if the difference in times is 2 minutes but arguments are flipped', () => { - const diff = calculateBucketForHour( - Date.parse('2020-02-20T05:57:54.037Z'), - Date.parse('2020-02-20T05:56:54.037Z') - ); - - expect(diff).toEqual(2); - }); - }); - - describe('calculateBucketForDay', () => { - test('returns 0 if two dates are equivalent', () => { - const diff = calculateBucketForDay( - Date.parse('2020-02-20T05:57:54.037Z'), - Date.parse('2020-02-20T05:57:54.037Z') - ); - - expect(diff).toEqual(0); - }); - - test('returns 1 if the difference in times is 60 minutes', () => { - const diff = calculateBucketForDay( - Date.parse('2020-02-20T05:17:54.037Z'), - Date.parse('2020-02-20T05:57:54.037Z') - ); - - expect(diff).toEqual(1); - }); - - test('returns 2 if the difference in times is 60-120 minutes', () => { - const diff = calculateBucketForDay( - Date.parse('2020-02-20T03:57:54.037Z'), - Date.parse('2020-02-20T05:57:54.037Z') - ); - - expect(diff).toEqual(2); - }); - - test('returns 3 if the difference in times is 120-180 minutes', () => { - const diff = calculateBucketForDay( - Date.parse('2020-02-20T03:56:54.037Z'), - Date.parse('2020-02-20T05:57:54.037Z') - ); - - expect(diff).toEqual(3); - }); - - test('returns 4 if the difference in times is 180-240 minutes', () => { - const diff = calculateBucketForDay( - Date.parse('2020-02-20T02:15:54.037Z'), - Date.parse('2020-02-20T05:57:54.037Z') - ); - - expect(diff).toEqual(4); - }); - - test('returns 2 if the difference in times is 60-120 minutes but arguments are flipped', () => { - const diff = calculateBucketForDay( - Date.parse('2020-02-20T05:57:54.037Z'), - Date.parse('2020-02-20T03:59:54.037Z') - ); - - expect(diff).toEqual(2); - }); - }); - - describe('getEqlAggsData', () => { - describe('non-sequence', () => { - // NOTE: We previously expected @timestamp to be a string, however, - // date can also be a number (like for endgame-*) - test('it works when @timestamp is a number', () => { - const mockResponse = getMockEndgameEqlResponse(); - - const aggs = getEqlAggsData( - mockResponse, - 'h', - '2020-10-04T16:00:00.368707900Z', - jest.fn() as inputsModel.Refetch, - ['foo-*'], - false - ); - - const date1 = moment(aggs.data[0].x); - const date2 = moment(aggs.data[1].x); - // This will be in ms - const diff = date1.diff(date2); - - expect(diff).toEqual(120000); - expect(aggs.data).toHaveLength(31); - expect(aggs.data).toEqual([ - { g: 'hits', x: 1601827200368, y: 0 }, - { g: 'hits', x: 1601827080368, y: 0 }, - { g: 'hits', x: 1601826960368, y: 0 }, - { g: 'hits', x: 1601826840368, y: 0 }, - { g: 'hits', x: 1601826720368, y: 0 }, - { g: 'hits', x: 1601826600368, y: 1 }, - { g: 'hits', x: 1601826480368, y: 0 }, - { g: 'hits', x: 1601826360368, y: 0 }, - { g: 'hits', x: 1601826240368, y: 0 }, - { g: 'hits', x: 1601826120368, y: 0 }, - { g: 'hits', x: 1601826000368, y: 0 }, - { g: 'hits', x: 1601825880368, y: 0 }, - { g: 'hits', x: 1601825760368, y: 0 }, - { g: 'hits', x: 1601825640368, y: 0 }, - { g: 'hits', x: 1601825520368, y: 0 }, - { g: 'hits', x: 1601825400368, y: 0 }, - { g: 'hits', x: 1601825280368, y: 0 }, - { g: 'hits', x: 1601825160368, y: 0 }, - { g: 'hits', x: 1601825040368, y: 0 }, - { g: 'hits', x: 1601824920368, y: 0 }, - { g: 'hits', x: 1601824800368, y: 0 }, - { g: 'hits', x: 1601824680368, y: 0 }, - { g: 'hits', x: 1601824560368, y: 2 }, - { g: 'hits', x: 1601824440368, y: 0 }, - { g: 'hits', x: 1601824320368, y: 0 }, - { g: 'hits', x: 1601824200368, y: 0 }, - { g: 'hits', x: 1601824080368, y: 0 }, - { g: 'hits', x: 1601823960368, y: 1 }, - { g: 'hits', x: 1601823840368, y: 0 }, - { g: 'hits', x: 1601823720368, y: 0 }, - { g: 'hits', x: 1601823600368, y: 0 }, - ]); - }); - - test('it returns results bucketed into 2 min intervals when range is "h"', () => { - const mockResponse = getMockEqlResponse(); - - const aggs = getEqlAggsData( - mockResponse, - 'h', - '2020-10-04T16:00:00.368707900Z', - jest.fn() as inputsModel.Refetch, - ['foo-*'], - false - ); - - const date1 = moment(aggs.data[0].x); - const date2 = moment(aggs.data[1].x); - // This will be in ms - const diff = date1.diff(date2); - - expect(diff).toEqual(120000); - expect(aggs.data).toHaveLength(31); - expect(aggs.data).toEqual([ - { g: 'hits', x: 1601827200368, y: 0 }, - { g: 'hits', x: 1601827080368, y: 0 }, - { g: 'hits', x: 1601826960368, y: 0 }, - { g: 'hits', x: 1601826840368, y: 0 }, - { g: 'hits', x: 1601826720368, y: 0 }, - { g: 'hits', x: 1601826600368, y: 1 }, - { g: 'hits', x: 1601826480368, y: 0 }, - { g: 'hits', x: 1601826360368, y: 0 }, - { g: 'hits', x: 1601826240368, y: 0 }, - { g: 'hits', x: 1601826120368, y: 0 }, - { g: 'hits', x: 1601826000368, y: 0 }, - { g: 'hits', x: 1601825880368, y: 0 }, - { g: 'hits', x: 1601825760368, y: 0 }, - { g: 'hits', x: 1601825640368, y: 0 }, - { g: 'hits', x: 1601825520368, y: 0 }, - { g: 'hits', x: 1601825400368, y: 0 }, - { g: 'hits', x: 1601825280368, y: 0 }, - { g: 'hits', x: 1601825160368, y: 0 }, - { g: 'hits', x: 1601825040368, y: 0 }, - { g: 'hits', x: 1601824920368, y: 0 }, - { g: 'hits', x: 1601824800368, y: 0 }, - { g: 'hits', x: 1601824680368, y: 0 }, - { g: 'hits', x: 1601824560368, y: 2 }, - { g: 'hits', x: 1601824440368, y: 0 }, - { g: 'hits', x: 1601824320368, y: 0 }, - { g: 'hits', x: 1601824200368, y: 0 }, - { g: 'hits', x: 1601824080368, y: 0 }, - { g: 'hits', x: 1601823960368, y: 1 }, - { g: 'hits', x: 1601823840368, y: 0 }, - { g: 'hits', x: 1601823720368, y: 0 }, - { g: 'hits', x: 1601823600368, y: 0 }, - ]); - }); - - test('it returns results bucketed into 1 hour intervals when range is "d"', () => { - const mockResponse = getMockEqlResponse(); - const response: EqlSearchStrategyResponse> = { - ...mockResponse, - rawResponse: { - ...mockResponse.rawResponse, - body: { - is_partial: false, - is_running: false, - timed_out: false, - took: 15, - hits: { - events: [ - { - _index: 'index', - _id: '1', - _source: { - '@timestamp': '2020-10-04T15:16:54.368707900Z', - }, - }, - { - _index: 'index', - _id: '2', - _source: { - '@timestamp': '2020-10-04T05:50:54.368707900Z', - }, - }, - { - _index: 'index', - _id: '3', - _source: { - '@timestamp': '2020-10-04T18:06:54.368707900Z', - }, - }, - { - _index: 'index', - _id: '4', - _source: { - '@timestamp': '2020-10-04T23:15:54.368707900Z', - }, - }, - ], - total: { - value: 4, - relation: '', - }, - }, - }, - }, - }; - - const aggs = getEqlAggsData( - response, - 'd', - '2020-10-04T23:50:00.368707900Z', - jest.fn() as inputsModel.Refetch, - ['foo-*'], - false - ); - const date1 = moment(aggs.data[0].x); - const date2 = moment(aggs.data[1].x); - // This'll be in ms - const diff = date1.diff(date2); - - expect(diff).toEqual(3600000); - expect(aggs.data).toHaveLength(25); - expect(aggs.data).toEqual([ - { g: 'hits', x: 1601855400368, y: 0 }, - { g: 'hits', x: 1601851800368, y: 1 }, - { g: 'hits', x: 1601848200368, y: 0 }, - { g: 'hits', x: 1601844600368, y: 0 }, - { g: 'hits', x: 1601841000368, y: 0 }, - { g: 'hits', x: 1601837400368, y: 0 }, - { g: 'hits', x: 1601833800368, y: 1 }, - { g: 'hits', x: 1601830200368, y: 0 }, - { g: 'hits', x: 1601826600368, y: 0 }, - { g: 'hits', x: 1601823000368, y: 1 }, - { g: 'hits', x: 1601819400368, y: 0 }, - { g: 'hits', x: 1601815800368, y: 0 }, - { g: 'hits', x: 1601812200368, y: 0 }, - { g: 'hits', x: 1601808600368, y: 0 }, - { g: 'hits', x: 1601805000368, y: 0 }, - { g: 'hits', x: 1601801400368, y: 0 }, - { g: 'hits', x: 1601797800368, y: 0 }, - { g: 'hits', x: 1601794200368, y: 0 }, - { g: 'hits', x: 1601790600368, y: 1 }, - { g: 'hits', x: 1601787000368, y: 0 }, - { g: 'hits', x: 1601783400368, y: 0 }, - { g: 'hits', x: 1601779800368, y: 0 }, - { g: 'hits', x: 1601776200368, y: 0 }, - { g: 'hits', x: 1601772600368, y: 0 }, - { g: 'hits', x: 1601769000368, y: 0 }, - ]); - }); - - test('it correctly returns total hits', () => { - const mockResponse = getMockEqlResponse(); - - const aggs = getEqlAggsData( - mockResponse, - 'h', - '2020-10-04T16:00:00.368707900Z', - jest.fn() as inputsModel.Refetch, - ['foo-*'], - false - ); - - expect(aggs.totalCount).toEqual(4); - }); - - test('it returns array with each item having a "total" of 0 if response returns no hits', () => { - const mockResponse = getMockEqlResponse(); - const response: EqlSearchStrategyResponse> = { - ...mockResponse, - rawResponse: { - ...mockResponse.rawResponse, - body: { - is_partial: false, - is_running: false, - timed_out: false, - took: 15, - hits: { - total: { - value: 0, - relation: '', - }, - }, - }, - }, - }; - - const aggs = getEqlAggsData( - response, - 'h', - '2020-10-04T16:00:00.368707900Z', - jest.fn() as inputsModel.Refetch, - ['foo-*'], - false - ); - - expect(aggs.data.every(({ y }) => y === 0)).toBeTruthy(); - expect(aggs.totalCount).toEqual(0); - }); - }); - - describe('sequence', () => { - test('it returns results bucketed into 2 min intervals when range is "h"', () => { - const mockResponse = getMockEqlSequenceResponse(); - - const aggs = getEqlAggsData( - mockResponse, - 'h', - '2020-10-04T16:00:00.368707900Z', - jest.fn() as inputsModel.Refetch, - ['foo-*'], - true - ); - - const date1 = moment(aggs.data[0].x); - const date2 = moment(aggs.data[1].x); - // This will be in ms - const diff = date1.diff(date2); - - expect(diff).toEqual(120000); - expect(aggs.data).toHaveLength(31); - expect(aggs.data).toEqual([ - { g: 'hits', x: 1601827200368, y: 0 }, - { g: 'hits', x: 1601827080368, y: 0 }, - { g: 'hits', x: 1601826960368, y: 0 }, - { g: 'hits', x: 1601826840368, y: 0 }, - { g: 'hits', x: 1601826720368, y: 0 }, - { g: 'hits', x: 1601826600368, y: 1 }, - { g: 'hits', x: 1601826480368, y: 0 }, - { g: 'hits', x: 1601826360368, y: 0 }, - { g: 'hits', x: 1601826240368, y: 0 }, - { g: 'hits', x: 1601826120368, y: 0 }, - { g: 'hits', x: 1601826000368, y: 0 }, - { g: 'hits', x: 1601825880368, y: 0 }, - { g: 'hits', x: 1601825760368, y: 0 }, - { g: 'hits', x: 1601825640368, y: 0 }, - { g: 'hits', x: 1601825520368, y: 0 }, - { g: 'hits', x: 1601825400368, y: 0 }, - { g: 'hits', x: 1601825280368, y: 0 }, - { g: 'hits', x: 1601825160368, y: 0 }, - { g: 'hits', x: 1601825040368, y: 0 }, - { g: 'hits', x: 1601824920368, y: 0 }, - { g: 'hits', x: 1601824800368, y: 0 }, - { g: 'hits', x: 1601824680368, y: 0 }, - { g: 'hits', x: 1601824560368, y: 1 }, - { g: 'hits', x: 1601824440368, y: 0 }, - { g: 'hits', x: 1601824320368, y: 0 }, - { g: 'hits', x: 1601824200368, y: 0 }, - { g: 'hits', x: 1601824080368, y: 0 }, - { g: 'hits', x: 1601823960368, y: 0 }, - { g: 'hits', x: 1601823840368, y: 0 }, - { g: 'hits', x: 1601823720368, y: 0 }, - { g: 'hits', x: 1601823600368, y: 0 }, - ]); - }); - - test('it returns results bucketed into 1 hour intervals when range is "d"', () => { - const mockResponse = getMockEqlSequenceResponse(); - const response: EqlSearchStrategyResponse> = { - ...mockResponse, - rawResponse: { - ...mockResponse.rawResponse, - body: { - is_partial: false, - is_running: false, - timed_out: false, - took: 15, - hits: { - sequences: [ - { - join_keys: [], - events: [ - { - _index: 'index', - _id: '1', - _source: { - '@timestamp': '2020-10-04T15:16:54.368707900Z', - }, - }, - { - _index: 'index', - _id: '2', - _source: { - '@timestamp': '2020-10-04T05:50:54.368707900Z', - }, - }, - ], - }, - { - join_keys: [], - events: [ - { - _index: 'index', - _id: '3', - _source: { - '@timestamp': '2020-10-04T18:06:54.368707900Z', - }, - }, - { - _index: 'index', - _id: '4', - _source: { - '@timestamp': '2020-10-04T23:15:54.368707900Z', - }, - }, - ], - }, - ], - total: { - value: 4, - relation: '', - }, - }, - }, - }, - }; - - const aggs = getEqlAggsData( - response, - 'd', - '2020-10-04T23:50:00.368707900Z', - jest.fn() as inputsModel.Refetch, - ['foo-*'], - true - ); - const date1 = moment(aggs.data[0].x); - const date2 = moment(aggs.data[1].x); - // This'll be in ms - const diff = date1.diff(date2); - - expect(diff).toEqual(3600000); - expect(aggs.data).toHaveLength(25); - expect(aggs.data).toEqual([ - { g: 'hits', x: 1601855400368, y: 0 }, - { g: 'hits', x: 1601851800368, y: 1 }, - { g: 'hits', x: 1601848200368, y: 0 }, - { g: 'hits', x: 1601844600368, y: 0 }, - { g: 'hits', x: 1601841000368, y: 0 }, - { g: 'hits', x: 1601837400368, y: 0 }, - { g: 'hits', x: 1601833800368, y: 0 }, - { g: 'hits', x: 1601830200368, y: 0 }, - { g: 'hits', x: 1601826600368, y: 0 }, - { g: 'hits', x: 1601823000368, y: 0 }, - { g: 'hits', x: 1601819400368, y: 0 }, - { g: 'hits', x: 1601815800368, y: 0 }, - { g: 'hits', x: 1601812200368, y: 0 }, - { g: 'hits', x: 1601808600368, y: 0 }, - { g: 'hits', x: 1601805000368, y: 0 }, - { g: 'hits', x: 1601801400368, y: 0 }, - { g: 'hits', x: 1601797800368, y: 0 }, - { g: 'hits', x: 1601794200368, y: 0 }, - { g: 'hits', x: 1601790600368, y: 1 }, - { g: 'hits', x: 1601787000368, y: 0 }, - { g: 'hits', x: 1601783400368, y: 0 }, - { g: 'hits', x: 1601779800368, y: 0 }, - { g: 'hits', x: 1601776200368, y: 0 }, - { g: 'hits', x: 1601772600368, y: 0 }, - { g: 'hits', x: 1601769000368, y: 0 }, - ]); - }); - - test('it correctly returns total hits', () => { - const mockResponse = getMockEqlSequenceResponse(); - - const aggs = getEqlAggsData( - mockResponse, - 'h', - '2020-10-04T16:00:00.368707900Z', - jest.fn() as inputsModel.Refetch, - ['foo-*'], - true - ); - - expect(aggs.totalCount).toEqual(4); - }); - - test('it returns array with each item having a "total" of 0 if response returns no hits', () => { - const mockResponse = getMockEqlSequenceResponse(); - const response: EqlSearchStrategyResponse> = { - ...mockResponse, - rawResponse: { - ...mockResponse.rawResponse, - body: { - is_partial: false, - is_running: false, - timed_out: false, - took: 15, - hits: { - total: { - value: 0, - relation: '', - }, - }, - }, - }, - }; - - const aggs = getEqlAggsData( - response, - 'h', - '2020-10-04T16:00:00.368707900Z', - jest.fn() as inputsModel.Refetch, - ['foo-*'], - true - ); - - expect(aggs.data.every(({ y }) => y === 0)).toBeTruthy(); - expect(aggs.totalCount).toEqual(0); - }); - }); - }); - - describe('createIntervalArray', () => { - test('returns array of 12 numbers from 0 to 60 by 5', () => { - const arrayOfNumbers = createIntervalArray(0, 12, 5); - expect(arrayOfNumbers).toEqual([0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60]); - }); - - test('returns array of 5 numbers from 0 to 10 by 2', () => { - const arrayOfNumbers = createIntervalArray(0, 5, 2); - expect(arrayOfNumbers).toEqual([0, 2, 4, 6, 8, 10]); - }); - - test('returns array of numbers from start param to end param if multiplier is 1', () => { - const arrayOfNumbers = createIntervalArray(0, 12, 1); - expect(arrayOfNumbers).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - }); - }); - - describe('getInterval', () => { - test('returns object with 2 minute interval timestamps if range is "h"', () => { - const intervals = getInterval('h', 1601856270140); - - const allAre2MinApart = Object.keys(intervals).every((int) => { - const interval1 = intervals[int]; - const interval2 = intervals[`${Number(int) + 2}`]; - if (interval1 != null && interval2 != null) { - const date1 = moment(Number(interval1.timestamp)); - const date2 = moment(Number(interval2.timestamp)); - // This'll be in ms - const diff = date1.diff(date2); - - return diff === 120000; - } - - return true; - }); - - expect(allAre2MinApart).toBeTruthy(); - }); - - test('returns object with 1 hour interval timestamps if range is "d"', () => { - const intervals = getInterval('d', 1601856270140); - - const allAre1HourApart = Object.keys(intervals).every((int) => { - const interval1 = intervals[int]; - const interval2 = intervals[`${Number(int) + 1}`]; - if (interval1 != null && interval2 != null) { - const date1 = moment(Number(interval1.timestamp)); - const date2 = moment(Number(interval2.timestamp)); - // This'll be in ms - const diff = date1.diff(date2); - - return diff === 3600000; - } - - return true; - }); - - expect(allAre1HourApart).toBeTruthy(); - }); - - test('returns error if range is anything other than "h" or "d"', () => { - expect(() => getInterval('m', 1601856270140)).toThrow(); - }); - }); - - describe('formatInspect', () => { - test('it should return "dsl" with response params and index info', () => { - const { dsl } = formatInspect(getMockEqlResponse(), ['foo-*']); - - expect(JSON.parse(dsl[0])).toEqual({ - body: { - filter: { - range: { - '@timestamp': { - format: 'strict_date_optional_time', - gte: '2020-10-07T00:46:12.414Z', - lte: '2020-10-07T01:46:12.414Z', - }, - }, - }, - }, - index: ['foo-*'], - method: 'GET', - path: '/_eql/search/', - querystring: 'some query string', - }); - }); - - test('it should return "response"', () => { - const mockResponse = getMockEqlResponse(); - const { response } = formatInspect(mockResponse, ['foo-*']); - - expect(JSON.parse(response[0])).toEqual(mockResponse.rawResponse.body); - }); - }); - - describe('getEventsToBucket', () => { - test('returns events for non-sequence queries', () => { - const events = getEventsToBucket(false, getMockEqlResponse()); - - expect(events).toEqual([ - { _id: '1', _index: 'index', _source: { '@timestamp': '2020-10-04T15:16:54.368707900Z' } }, - { _id: '2', _index: 'index', _source: { '@timestamp': '2020-10-04T15:50:54.368707900Z' } }, - { _id: '3', _index: 'index', _source: { '@timestamp': '2020-10-04T15:06:54.368707900Z' } }, - { _id: '4', _index: 'index', _source: { '@timestamp': '2020-10-04T15:15:54.368707900Z' } }, - ]); - }); - - test('returns empty array if no hits', () => { - const resp = getMockEqlResponse(); - const mockResponse: EqlSearchStrategyResponse> = { - ...resp, - rawResponse: { - ...resp.rawResponse, - body: { - ...resp.rawResponse.body, - hits: { - total: { - value: 0, - relation: '', - }, - }, - }, - }, - }; - const events = getEventsToBucket(false, mockResponse); - - expect(events).toEqual([]); - }); - - test('returns events for sequence queries', () => { - const events = getEventsToBucket(true, getMockEqlSequenceResponse()); - - expect(events).toEqual([ - { _id: '2', _index: 'index', _source: { '@timestamp': '2020-10-04T15:50:54.368707900Z' } }, - { _id: '4', _index: 'index', _source: { '@timestamp': '2020-10-04T15:15:54.368707900Z' } }, - ]); - }); - - test('returns empty array if no sequences', () => { - const resp = getMockEqlSequenceResponse(); - const mockResponse: EqlSearchStrategyResponse> = { - ...resp, - rawResponse: { - ...resp.rawResponse, - body: { - ...resp.rawResponse.body, - hits: { - total: { - value: 0, - relation: '', - }, - }, - }, - }, - }; - const events = getEventsToBucket(true, mockResponse); - - expect(events).toEqual([]); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts deleted file mode 100644 index 87150062c368..000000000000 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import type { Unit } from '@kbn/datemath'; -import type { EqlSearchStrategyResponse } from '@kbn/data-plugin/common'; -import type { inputsModel } from '../../store'; - -import type { InspectResponse } from '../../../types'; -import type { EqlPreviewResponse, Source } from './types'; -import type { BaseHit, EqlSearchResponse } from '../../../../common/detection_engine/types'; - -type EqlAggBuckets = Record; - -/** - * Calculates which 2 min bucket segment, event should be sorted into - * @param eventTimestamp The event to be bucketed timestamp - * @param relativeNow The timestamp we are using to calculate how far from 'now' event occurred - */ -export const calculateBucketForHour = (eventTimestamp: number, relativeNow: number): number => { - const diff = Math.abs(relativeNow - eventTimestamp); - const minutes = Math.floor(diff / 60000); - return Math.ceil(minutes / 2) * 2; -}; - -/** - * Calculates which 1 hour bucket segment, event should be sorted into - * @param eventTimestamp The event to be bucketed timestamp - * @param relativeNow The timestamp we are using to calculate how far from 'now' event occurred - */ -export const calculateBucketForDay = (eventTimestamp: number, relativeNow: number): number => { - const diff = Math.abs(relativeNow - eventTimestamp); - const minutes = Math.floor(diff / 60000); - return Math.ceil(minutes / 60); -}; - -/** - * Formats the response for the UI inspect modal - * @param response The query search response - * @param indices The indices the query searched - * TODO: Update eql search strategy to return index in it's meta - * params info, currently not being returned, but expected for - * inspect modal display - */ -export const formatInspect = ( - response: EqlSearchStrategyResponse>, - indices: string[] -): InspectResponse => { - const body = response.rawResponse.meta.request.params.body; - const bodyParse: Record | undefined = - typeof body === 'string' ? JSON.parse(body) : body; - return { - dsl: [ - JSON.stringify( - { ...response.rawResponse.meta.request.params, index: indices, body: bodyParse }, - null, - 2 - ), - ], - response: [JSON.stringify(response.rawResponse.body, null, 2)], - }; -}; - -/** - * Gets the events out of the response based on type of query - * @param isSequence Is the eql query a sequence query - * @param response The query search response - */ -export const getEventsToBucket = ( - isSequence: boolean, - response: EqlSearchStrategyResponse> -): Array> => { - const hits = response.rawResponse.body.hits ?? []; - if (isSequence) { - return ( - hits.sequences?.map((seq) => { - return seq.events[seq.events.length - 1]; - }) ?? [] - ); - } else { - return hits.events ?? []; - } -}; - -/** - * Eql does not support aggregations, this is an in-memory - * hand-spun aggregation for the events to give the user a visual - * representation of their query results - * @param response The query search response - * @param range User chosen timeframe (last hour, day) - * @param to Based on range chosen - * @param refetch Callback used in inspect button, ref just passed through - * @param indices Indices searched by query - * @param isSequence Is the eql query a sequence query - */ -export const getEqlAggsData = ( - response: EqlSearchStrategyResponse>, - range: Unit, - to: string, - refetch: inputsModel.Refetch, - indices: string[], - isSequence: boolean -): EqlPreviewResponse => { - const { dsl, response: inspectResponse } = formatInspect(response, indices); - const relativeNow = Date.parse(to); - const accumulator = getInterval(range, relativeNow); - const events = getEventsToBucket(isSequence, response); - const totalCount = response.rawResponse.body.hits.total.value; - - const buckets = events.reduce((acc, hit) => { - const timestamp = hit._source['@timestamp']; - if (timestamp == null) { - return acc; - } - const eventDate = new Date(timestamp).toISOString(); - const eventTimestamp = Date.parse(eventDate); - const bucket = - range === 'h' - ? calculateBucketForHour(eventTimestamp, relativeNow) - : calculateBucketForDay(eventTimestamp, relativeNow); - if (acc[bucket] != null) { - acc[bucket].total += 1; - } - return acc; - }, accumulator); - const data = Object.keys(buckets).map((key) => { - return { x: Number(buckets[key].timestamp), y: buckets[key].total, g: 'hits' }; - }); - - const isAllZeros = data.every(({ y }) => y === 0); - - return { - data, - totalCount: isAllZeros ? 0 : totalCount, - inspect: { - dsl, - response: inspectResponse, - }, - refetch, - }; -}; - -/** - * Helper method to create an array to be used for calculating bucket intervals - * @param start - * @param end - * @param multiplier - */ -export const createIntervalArray = (start: number, end: number, multiplier: number): number[] => { - return Array(end - start + 1) - .fill(0) - .map((_, idx) => start + idx * multiplier); -}; - -/** - * Helper method to create an array to be used for calculating bucket intervals - * @param range User chosen timeframe (last hour, day) - * @param relativeNow Based on range chosen - */ -export const getInterval = (range: Unit, relativeNow: number): EqlAggBuckets => { - switch (range) { - case 'h': - return createIntervalArray(0, 30, 2).reduce((acc, int) => { - return { - ...acc, - [int]: { timestamp: moment(relativeNow).subtract(int, 'm').format('x'), total: 0 }, - }; - }, {}); - case 'd': - return createIntervalArray(0, 24, 1).reduce((acc, int) => { - return { - ...acc, - [int]: { timestamp: moment(relativeNow).subtract(int, 'h').format('x'), total: 0 }, - }; - }, {}); - default: - throw new RangeError('Invalid time range selected. Must be "Last hour" or "Last day".'); - } -}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/types.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/types.ts deleted file mode 100644 index d46f79c286a7..000000000000 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Unit } from '@kbn/datemath'; - -import type { InspectResponse } from '../../../types'; -import type { ChartData } from '../../components/charts/common'; -import type { inputsModel } from '../../store'; - -export interface EqlPreviewRequest { - to: string; - from: string; - interval: Unit; - query: string; - index: string[]; -} - -export interface EqlPreviewResponse { - data: ChartData[]; - totalCount: number; - inspect: InspectResponse; - refetch: inputsModel.Refetch; -} - -export interface Source { - '@timestamp': string | number; -} diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts deleted file mode 100644 index 166d89560e4d..000000000000 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Unit } from '@kbn/datemath'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { of, throwError } from 'rxjs'; -import { delay } from 'rxjs/operators'; - -import * as i18n from '../translations'; -import type { EqlSearchStrategyResponse } from '@kbn/data-plugin/common'; -import type { Source } from './types'; -import type { EqlSearchResponse } from '../../../../common/detection_engine/types'; -import { useKibana } from '../../lib/kibana'; -import { useEqlPreview } from '.'; -import { getMockEqlResponse } from './eql_search_response.mock'; -import { useAppToasts } from '../use_app_toasts'; - -jest.mock('../../lib/kibana'); -jest.mock('../use_app_toasts'); - -describe('useEqlPreview', () => { - const params = { - to: '2020-10-04T16:00:54.368707900Z', - query: 'file where true', - index: ['foo-*', 'bar-*'], - interval: 'h' as Unit, - from: '2020-10-04T15:00:54.368707900Z', - }; - - let addErrorMock: jest.Mock; - let addSuccessMock: jest.Mock; - let addWarningMock: jest.Mock; - - beforeEach(() => { - addErrorMock = jest.fn(); - addSuccessMock = jest.fn(); - addWarningMock = jest.fn(); - (useAppToasts as jest.Mock).mockImplementation(() => ({ - addError: addErrorMock, - addWarning: addWarningMock, - addSuccess: addSuccessMock, - })); - - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ); - }); - - it('should initiate hook', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); - await waitForNextUpdate(); - - expect(result.current[0]).toBeFalsy(); - expect(typeof result.current[1]).toEqual('function'); - expect(result.current[2]).toEqual({ - data: [], - inspect: { dsl: [], response: [] }, - refetch: result.current[2].refetch, - totalCount: 0, - }); - }); - }); - - it('should invoke search with passed in params', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); - - await waitForNextUpdate(); - - result.current[1](params); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(mockCalls[0][0].params.body.query).toEqual('file where true'); - expect(mockCalls[0][0].params.body.filter).toEqual({ - range: { - '@timestamp': { - format: 'strict_date_optional_time', - gte: '2020-10-04T15:00:54.368707900Z', - lte: '2020-10-04T16:00:54.368707900Z', - }, - }, - }); - expect(mockCalls[0][0].params.index).toBe('foo-*,bar-*'); - }); - }); - - it('should resolve values after search is invoked', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); - - await waitForNextUpdate(); - - result.current[1](params); - - expect(result.current[0]).toBeFalsy(); - expect(typeof result.current[1]).toEqual('function'); - expect(result.current[2].totalCount).toEqual(4); - expect(result.current[2].data.length).toBeGreaterThan(0); - expect(result.current[2].inspect.dsl.length).toBeGreaterThan(0); - expect(result.current[2].inspect.response.length).toBeGreaterThan(0); - }); - }); - - it('should not resolve values after search is invoked if component unmounted', async () => { - await act(async () => { - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()).pipe(delay(5000)) - ); - const { result, waitForNextUpdate, unmount } = renderHook(() => useEqlPreview()); - - await waitForNextUpdate(); - - result.current[1](params); - - unmount(); - - expect(result.current[0]).toBeTruthy(); - expect(result.current[2].totalCount).toEqual(0); - expect(result.current[2].data.length).toEqual(0); - expect(result.current[2].inspect.dsl.length).toEqual(0); - expect(result.current[2].inspect.response.length).toEqual(0); - }); - }); - - it('should not resolve new values on search if response is error response', async () => { - await act(async () => { - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of>>({ - ...getMockEqlResponse(), - isRunning: false, - isPartial: true, - }) - ); - - const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); - - await waitForNextUpdate(); - - result.current[1](params); - - expect(result.current[0]).toBeFalsy(); - expect(addWarningMock.mock.calls[0][0]).toEqual(i18n.EQL_PREVIEW_FETCH_FAILURE); - }); - }); - - // TODO: Determine why eql search strategy returns null for meta.params.body - // in complete responses, but not in partial responses - it('should update inspect information on partial response', async () => { - const mockResponse = getMockEqlResponse(); - await act(async () => { - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of>>({ - isRunning: true, - isPartial: true, - rawResponse: mockResponse.rawResponse, - }) - ); - - const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); - - await waitForNextUpdate(); - - result.current[1](params); - - expect(result.current[2].inspect.dsl.length).toEqual(1); - expect(result.current[2].inspect.response.length).toEqual(1); - }); - }); - - it('should add error toast if search throws', async () => { - await act(async () => { - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - throwError('This is an error!') - ); - - const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); - - await waitForNextUpdate(); - - result.current[1](params); - - expect(result.current[0]).toBeFalsy(); - expect(addErrorMock.mock.calls[0][0]).toEqual('This is an error!'); - }); - }); - - it('returns a memoized value', async () => { - const { result, rerender } = renderHook(() => useEqlPreview()); - - const result1 = result.current[1]; - act(() => rerender()); - const result2 = result.current[1]; - - expect(result1).toBe(result2); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts deleted file mode 100644 index bd04056509b8..000000000000 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useCallback, useEffect, useRef, useState } from 'react'; -import { noop } from 'lodash/fp'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; - -import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; -import type { EqlSearchStrategyRequest, EqlSearchStrategyResponse } from '@kbn/data-plugin/common'; -import { - isCompleteResponse, - isErrorResponse, - isPartialResponse, - EQL_SEARCH_STRATEGY, -} from '@kbn/data-plugin/common'; -import { AbortError } from '@kbn/kibana-utils-plugin/common'; -import * as i18n from '../translations'; -import { useKibana } from '../../lib/kibana'; -import { formatInspect, getEqlAggsData } from './helpers'; -import type { EqlPreviewResponse, EqlPreviewRequest, Source } from './types'; -import { hasEqlSequenceQuery } from '../../../../common/detection_engine/utils'; -import type { EqlSearchResponse } from '../../../../common/detection_engine/types'; -import type { inputsModel } from '../../store'; -import { useAppToasts } from '../use_app_toasts'; - -export const useEqlPreview = (): [ - boolean, - (arg: EqlPreviewRequest) => void, - EqlPreviewResponse -] => { - const { data } = useKibana().services; - const refetch = useRef(noop); - const abortCtrl = useRef(new AbortController()); - const unsubscribeStream = useRef(new Subject()); - const [loading, setLoading] = useState(false); - const didCancel = useRef(false); - const { addError, addWarning } = useAppToasts(); - - const [response, setResponse] = useState({ - data: [], - inspect: { - dsl: [], - response: [], - }, - refetch: refetch.current, - totalCount: 0, - }); - - const searchEql = useCallback( - ({ from, to, query, index, interval }: EqlPreviewRequest) => { - if (parseScheduleDates(to) == null || parseScheduleDates(from) == null) { - addWarning(i18n.EQL_TIME_INTERVAL_NOT_DEFINED); - return; - } - - const asyncSearch = async () => { - abortCtrl.current = new AbortController(); - setLoading(true); - setResponse((prevResponse) => ({ - ...prevResponse, - data: [], - inspect: { - dsl: [], - response: [], - }, - totalCount: 0, - })); - - data.search - .search>>( - { - params: { - index: index.join(), - body: { - filter: { - range: { - '@timestamp': { - gte: from, - lte: to, - format: 'strict_date_optional_time', - }, - }, - }, - query, - // EQL requires a cap, otherwise it defaults to 10 - // It also sorts on ascending order, capping it at - // something smaller like 20, made it so that some of - // the more recent events weren't returned - size: 100, - }, - }, - }, - { - strategy: EQL_SEARCH_STRATEGY, - abortSignal: abortCtrl.current.signal, - } - ) - .pipe(takeUntil(unsubscribeStream.current)) - .subscribe({ - next: (res) => { - if (isCompleteResponse(res)) { - if (!didCancel.current) { - setLoading(false); - - setResponse((prev) => { - const { inspect, ...rest } = getEqlAggsData( - res, - interval, - to, - refetch.current, - index, - hasEqlSequenceQuery(query) - ); - const inspectDsl = prev.inspect.dsl[0] ? prev.inspect.dsl : inspect.dsl; - const inspectResp = prev.inspect.response[0] - ? prev.inspect.response - : inspect.response; - - return { - ...prev, - ...rest, - inspect: { - dsl: inspectDsl, - response: inspectResp, - }, - }; - }); - } - - unsubscribeStream.current.next(); - } else if (isPartialResponse(res)) { - // TODO: Eql search strategy partial responses return a value under meta.params.body - // but the final/complete response does not, that's why the inspect values are set here - setResponse((prev) => ({ ...prev, inspect: formatInspect(res, index) })); - } else if (isErrorResponse(res)) { - setLoading(false); - addWarning(i18n.EQL_PREVIEW_FETCH_FAILURE); - unsubscribeStream.current.next(); - } - }, - error: (err) => { - if (!(err instanceof AbortError)) { - setLoading(false); - setResponse({ - data: [], - inspect: { - dsl: [], - response: [], - }, - refetch: refetch.current, - totalCount: 0, - }); - addError(err, { - title: i18n.EQL_PREVIEW_FETCH_FAILURE, - }); - } - }, - }); - }; - - abortCtrl.current.abort(); - asyncSearch(); - refetch.current = asyncSearch; - }, - [data.search, addError, addWarning] - ); - - useEffect((): (() => void) => { - return (): void => { - didCancel.current = true; - abortCtrl.current.abort(); - // eslint-disable-next-line react-hooks/exhaustive-deps - unsubscribeStream.current.complete(); - }; - }, []); - - return [loading, searchEql, response]; -}; diff --git a/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.test.ts b/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.test.ts index 6041df888dfc..4cf79cf4ce10 100644 --- a/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.test.ts +++ b/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.test.ts @@ -14,13 +14,18 @@ import { } from '../../../common/constants'; import { useKibana } from '../../common/lib/kibana'; import { useFetchSecurityTags } from './use_fetch_security_tags'; +import { DEFAULT_TAGS_RESPONSE } from '../../common/containers/tags/__mocks__/api'; +import type { ITagsClient } from '@kbn/saved-objects-tagging-plugin/common'; +import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; jest.mock('../../common/lib/kibana'); +jest.mock('../../../common/utils/get_ramdom_color', () => ({ + getRandomColor: jest.fn().mockReturnValue('#FFFFFF'), +})); const mockGet = jest.fn(); -const mockPut = jest.fn(); const mockAbortSignal = {} as unknown as AbortSignal; - +const mockCreateTag = jest.fn(); const renderUseCreateSecurityDashboardLink = () => renderHook(() => useFetchSecurityTags(), {}); const asyncRenderUseCreateSecurityDashboardLink = async () => { @@ -33,8 +38,10 @@ const asyncRenderUseCreateSecurityDashboardLink = async () => { describe('useFetchSecurityTags', () => { beforeAll(() => { - useKibana().services.http = { get: mockGet, put: mockPut } as unknown as HttpStart; - + useKibana().services.http = { get: mockGet } as unknown as HttpStart; + useKibana().services.savedObjectsTagging = { + client: { create: mockCreateTag } as unknown as ITagsClient, + } as unknown as SavedObjectsTaggingApi; global.AbortController = jest.fn().mockReturnValue({ abort: jest.fn(), signal: mockAbortSignal, @@ -59,18 +66,24 @@ describe('useFetchSecurityTags', () => { mockGet.mockResolvedValue([]); await asyncRenderUseCreateSecurityDashboardLink(); - expect(mockPut).toHaveBeenCalledWith(INTERNAL_TAGS_URL, { - body: JSON.stringify({ name: SECURITY_TAG_NAME, description: SECURITY_TAG_DESCRIPTION }), - signal: mockAbortSignal, + expect(mockCreateTag).toHaveBeenCalledWith({ + name: SECURITY_TAG_NAME, + description: SECURITY_TAG_DESCRIPTION, + color: '#FFFFFF', }); }); test('should return Security Solution tags', async () => { - const mockFoundTags = [{ id: 'tagId', name: 'Security Solution', description: '', color: '' }]; - mockGet.mockResolvedValue(mockFoundTags); + mockGet.mockResolvedValue(DEFAULT_TAGS_RESPONSE); + + const expected = DEFAULT_TAGS_RESPONSE.map((tag) => ({ + id: tag.id, + type: 'tag', + ...tag.attributes, + })); const { result } = await asyncRenderUseCreateSecurityDashboardLink(); - expect(mockPut).not.toHaveBeenCalled(); - expect(result.current.tags).toEqual(expect.objectContaining(mockFoundTags)); + expect(mockCreateTag).not.toHaveBeenCalled(); + expect(result.current.tags).toEqual(expect.objectContaining(expected)); }); }); diff --git a/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.ts b/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.ts index a91daf7be16e..9bdb3f891f59 100644 --- a/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.ts +++ b/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.ts @@ -10,9 +10,10 @@ import { useKibana } from '../../common/lib/kibana'; import { createTag, getTagsByName } from '../../common/containers/tags/api'; import { REQUEST_NAMES, useFetch } from '../../common/hooks/use_fetch'; import { SECURITY_TAG_DESCRIPTION, SECURITY_TAG_NAME } from '../../../common/constants'; +import { getRandomColor } from '../../../common/utils/get_ramdom_color'; export const useFetchSecurityTags = () => { - const { http } = useKibana().services; + const { http, savedObjectsTagging } = useKibana().services; const tagCreated = useRef(false); const { @@ -31,20 +32,31 @@ export const useFetchSecurityTags = () => { } = useFetch(REQUEST_NAMES.SECURITY_CREATE_TAG, createTag); useEffect(() => { - if (!isLoadingTags && !errorFetchTags && tags && tags.length === 0 && !tagCreated.current) { + if ( + savedObjectsTagging && + !isLoadingTags && + !errorFetchTags && + tags && + tags.length === 0 && + !tagCreated.current + ) { tagCreated.current = true; fetchCreateTag({ - http, - tag: { name: SECURITY_TAG_NAME, description: SECURITY_TAG_DESCRIPTION }, + savedObjectsTaggingClient: savedObjectsTagging.client, + tag: { + name: SECURITY_TAG_NAME, + description: SECURITY_TAG_DESCRIPTION, + color: getRandomColor(), + }, }); } - }, [errorFetchTags, fetchCreateTag, http, isLoadingTags, tags]); + }, [errorFetchTags, fetchCreateTag, savedObjectsTagging, isLoadingTags, tags]); const tagsResult = useMemo(() => { if (tags?.length) { - return tags; + return tags.map((t) => ({ id: t.id, type: 'tag', ...t.attributes })); } - return tag ? [tag] : undefined; + return tag ? [{ type: 'tag', ...tag }] : undefined; }, [tags, tag]); return { diff --git a/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx b/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx index 32aea030e063..cb8b40b1c090 100644 --- a/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx @@ -9,8 +9,11 @@ import React from 'react'; import type { Tag } from '@kbn/saved-objects-tagging-plugin/common'; import { useFetchSecurityTags } from '../containers/use_fetch_security_tags'; +export interface TagReference extends Tag { + type: string; +} export interface DashboardContextType { - securityTags: Tag[] | null; + securityTags: TagReference[] | null; } const DashboardContext = React.createContext({ securityTags: null }); diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx index 46c243eebb7c..357cf5d21670 100644 --- a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx @@ -5,20 +5,29 @@ * 2.0. */ -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import React from 'react'; import { SecurityPageName } from '../../../app/types'; import { TestProviders } from '../../../common/mock'; import { DashboardsLandingPage } from '.'; -import type { NavigationLink } from '../../../common/links'; import { useCapabilities } from '../../../common/lib/kibana'; import * as telemetry from '../../../common/lib/telemetry'; +import { DashboardListingTable } from '@kbn/dashboard-plugin/public'; +import { MOCK_TAG_NAME } from '../../../common/containers/tags/__mocks__/api'; +import { DashboardContextProvider } from '../../context/dashboard_context'; +import { act } from 'react-dom/test-utils'; +import type { NavigationLink } from '../../../common/links/types'; +jest.mock('../../../common/containers/tags/api'); jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/utils/route/spy_routes', () => ({ SpyRoute: () => null })); -jest.mock('../../components/dashboards_table', () => ({ - DashboardsTable: () => , -})); +jest.mock('@kbn/dashboard-plugin/public', () => { + const actual = jest.requireActual('@kbn/dashboard-plugin/public'); + return { + ...actual, + DashboardListingTable: jest.fn().mockReturnValue(), + }; +}); const DEFAULT_DASHBOARD_CAPABILITIES = { show: true, createNew: true }; const mockUseCapabilities = useCapabilities as jest.Mock; @@ -63,97 +72,122 @@ jest.mock('../../hooks/use_create_security_dashboard_link', () => { }; }); -const renderDashboardLanding = () => render(, { wrapper: TestProviders }); +const TestComponent = () => ( + + + + + +); + +const renderDashboardLanding = async () => { + await act(async () => { + render(); + }); +}; describe('Dashboards landing', () => { + beforeEach(() => { + mockUseCapabilities.mockReturnValue(DEFAULT_DASHBOARD_CAPABILITIES); + mockUseCreateSecurityDashboard.mockReturnValue(CREATE_DASHBOARD_LINK); + }); + describe('Dashboards default links', () => { - it('should render items', () => { - const { queryByText } = renderDashboardLanding(); + it('should render items', async () => { + await renderDashboardLanding(); - expect(queryByText(OVERVIEW_ITEM_LABEL)).toBeInTheDocument(); - expect(queryByText(DETECTION_RESPONSE_ITEM_LABEL)).toBeInTheDocument(); + expect(screen.queryByText(OVERVIEW_ITEM_LABEL)).toBeInTheDocument(); + expect(screen.queryByText(DETECTION_RESPONSE_ITEM_LABEL)).toBeInTheDocument(); }); - it('should render items in the same order as defined', () => { + it('should render items in the same order as defined', async () => { mockAppManageLink.mockReturnValueOnce({ ...APP_DASHBOARD_LINKS, }); - const { queryAllByTestId } = renderDashboardLanding(); + await renderDashboardLanding(); - const renderedItems = queryAllByTestId('LandingImageCard-item'); + const renderedItems = screen.queryAllByTestId('LandingImageCard-item'); expect(renderedItems[0]).toHaveTextContent(OVERVIEW_ITEM_LABEL); expect(renderedItems[1]).toHaveTextContent(DETECTION_RESPONSE_ITEM_LABEL); }); - it('should not render items if all items filtered', () => { - mockAppManageLink.mockReturnValueOnce({ + it('should not render items if all items filtered', async () => { + mockAppManageLink.mockReturnValue({ ...APP_DASHBOARD_LINKS, links: [], }); - const { queryByText } = renderDashboardLanding(); + await renderDashboardLanding(); - expect(queryByText(OVERVIEW_ITEM_LABEL)).not.toBeInTheDocument(); - expect(queryByText(DETECTION_RESPONSE_ITEM_LABEL)).not.toBeInTheDocument(); + expect(screen.queryByText(OVERVIEW_ITEM_LABEL)).not.toBeInTheDocument(); + expect(screen.queryByText(DETECTION_RESPONSE_ITEM_LABEL)).not.toBeInTheDocument(); }); }); describe('Security Dashboards', () => { - it('should render dashboards table', () => { - const result = renderDashboardLanding(); + it('should render dashboards table', async () => { + await renderDashboardLanding(); + + expect(screen.getByTestId('dashboardsTable')).toBeInTheDocument(); + }); + + it('should call DashboardListingTable with correct initialFilter', async () => { + await renderDashboardLanding(); - expect(result.getByTestId('dashboardsTable')).toBeInTheDocument(); + expect((DashboardListingTable as jest.Mock).mock.calls[0][0].initialFilter).toEqual( + `tag:("${MOCK_TAG_NAME}")` + ); }); - it('should not render dashboards table if no read capability', () => { - mockUseCapabilities.mockReturnValueOnce({ + it('should not render dashboards table if no read capability', async () => { + mockUseCapabilities.mockReturnValue({ ...DEFAULT_DASHBOARD_CAPABILITIES, show: false, }); - const result = renderDashboardLanding(); + await renderDashboardLanding(); - expect(result.queryByTestId('dashboardsTable')).not.toBeInTheDocument(); + expect(screen.queryByTestId('dashboardsTable')).not.toBeInTheDocument(); }); describe('Create Security Dashboard button', () => { - it('should render', () => { - const result = renderDashboardLanding(); + it('should render', async () => { + await renderDashboardLanding(); - expect(result.getByTestId('createDashboardButton')).toBeInTheDocument(); + expect(screen.getByTestId('createDashboardButton')).toBeInTheDocument(); }); - it('should not render if no write capability', () => { - mockUseCapabilities.mockReturnValueOnce({ + it('should not render if no write capability', async () => { + mockUseCapabilities.mockReturnValue({ ...DEFAULT_DASHBOARD_CAPABILITIES, createNew: false, }); - const result = renderDashboardLanding(); + await renderDashboardLanding(); - expect(result.queryByTestId('createDashboardButton')).not.toBeInTheDocument(); + expect(screen.queryByTestId('createDashboardButton')).not.toBeInTheDocument(); }); - it('should be enabled when link loaded', () => { - const result = renderDashboardLanding(); + it('should be enabled when link loaded', async () => { + await renderDashboardLanding(); - expect(result.getByTestId('createDashboardButton')).not.toHaveAttribute('disabled'); + expect(screen.getByTestId('createDashboardButton')).not.toHaveAttribute('disabled'); }); - it('should be disabled when link is not loaded', () => { - mockUseCreateSecurityDashboard.mockReturnValueOnce({ isLoading: true, url: '' }); - const result = renderDashboardLanding(); + it('should be disabled when link is not loaded', async () => { + mockUseCreateSecurityDashboard.mockReturnValue({ isLoading: true, url: '' }); + await renderDashboardLanding(); - expect(result.getByTestId('createDashboardButton')).toHaveAttribute('disabled'); + expect(screen.getByTestId('createDashboardButton')).toHaveAttribute('disabled'); }); - it('should link to correct href', () => { - const result = renderDashboardLanding(); + it('should link to correct href', async () => { + await renderDashboardLanding(); - expect(result.getByTestId('createDashboardButton')).toHaveAttribute('href', URL); + expect(screen.getByTestId('createDashboardButton')).toHaveAttribute('href', URL); }); - it('should send telemetry', () => { - const result = renderDashboardLanding(); - result.getByTestId('createDashboardButton').click(); + it('should send telemetry', async () => { + await renderDashboardLanding(); + screen.getByTestId('createDashboardButton').click(); expect(spyTrack).toHaveBeenCalledWith( telemetry.METRIC_TYPE.CLICK, telemetry.TELEMETRY_EVENT.CREATE_DASHBOARD diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx index 513ad89c482c..6da70442a1e3 100644 --- a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx @@ -4,10 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer, EuiTitle } from '@elastic/eui'; -import React from 'react'; +import { + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLoadingSpinner, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; import type { DashboardCapabilities } from '@kbn/dashboard-plugin/common/types'; -import { LEGACY_DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public'; +import { DashboardListingTable, LEGACY_DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public'; import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; import { LandingImageCards } from '../../../common/components/landing_links/landing_links_images'; @@ -20,7 +28,25 @@ import * as i18n from './translations'; import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../../common/lib/telemetry'; import { DASHBOARDS_PAGE_TITLE } from '../translations'; import { useCreateSecurityDashboardLink } from '../../hooks/use_create_security_dashboard_link'; -import { DashboardsTable } from '../../components/dashboards_table'; +import { useGetSecuritySolutionUrl } from '../../../common/components/link_to'; +import type { TagReference } from '../../context/dashboard_context'; +import { useSecurityTags } from '../../context/dashboard_context'; + +const getInitialFilterString = (securityTags: TagReference[] | null | undefined) => { + if (!securityTags) { + return; + } + const uniqueQuerySet = securityTags?.reduce>((acc, { name }) => { + const nameString = `"${name}"`; + if (name && !acc.has(nameString)) { + acc.add(nameString); + } + return acc; + }, new Set()); + + const query = [...uniqueQuerySet].join(' or'); + return `tag:(${query})`; +}; const Header: React.FC<{ canCreateDashboard: boolean }> = ({ canCreateDashboard }) => { const { isLoading, url } = useCreateSecurityDashboardLink(); @@ -57,7 +83,36 @@ export const DashboardsLandingPage = () => { const dashboardLinks = useRootNavLink(SecurityPageName.dashboards)?.links ?? []; const { show: canReadDashboard, createNew: canCreateDashboard } = useCapabilities(LEGACY_DASHBOARD_APP_ID); + const { navigateTo } = useNavigateTo(); + const getSecuritySolutionUrl = useGetSecuritySolutionUrl(); + const getSecuritySolutionDashboardUrl = useCallback( + (id: string) => + `${getSecuritySolutionUrl({ + deepLinkId: SecurityPageName.dashboards, + path: id, + })}`, + [getSecuritySolutionUrl] + ); + const { isLoading: loadingCreateDashboardUrl, url: createDashboardUrl } = + useCreateSecurityDashboardLink(); + const getHref = useCallback( + (id: string | undefined) => (id ? getSecuritySolutionDashboardUrl(id) : createDashboardUrl), + [createDashboardUrl, getSecuritySolutionDashboardUrl] + ); + + const goToDashboard = useCallback( + (dashboardId: string | undefined) => { + track(METRIC_TYPE.CLICK, TELEMETRY_EVENT.DASHBOARD); + navigateTo({ url: getHref(dashboardId) }); + }, + [getHref, navigateTo] + ); + + const securityTags = useSecurityTags(); + const securityTagsExist = securityTags && securityTags?.length > 0; + + const initialFilter = useMemo(() => getInitialFilterString(securityTags), [securityTags]); return (
@@ -68,17 +123,26 @@ export const DashboardsLandingPage = () => { - + - {canReadDashboard && ( + {canReadDashboard && securityTagsExist && initialFilter ? ( <> - -

{i18n.DASHBOARDS_PAGE_SECTION_CUSTOM}

-
- - - + + +

{i18n.DASHBOARDS_PAGE_SECTION_CUSTOM}

+
+ + +
+ ) : ( + } /> )} diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx b/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx index c423fc2eac6d..0786b4cdf382 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx @@ -19,7 +19,7 @@ import { inputsSelectors } from '../../../common/store'; import { formatPageFilterSearchParam } from '../../../../common/utils/format_page_filter_search_param'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { resolveFlyoutParams } from './utils'; -import { FLYOUT_URL_PARAM } from '../../../flyout/url/use_sync_flyout_state_with_url'; +import { FLYOUT_URL_PARAM } from '../../../flyout/shared/hooks/url/use_sync_flyout_state_with_url'; export const AlertDetailsRedirect = () => { const { alertId } = useParams<{ alertId: string }>(); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/alerts/utils.ts index db1250b4e6ca..c08f6fd6ac4e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alerts/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/utils.ts @@ -6,7 +6,7 @@ */ import { encode } from '@kbn/rison'; -import { expandableFlyoutStateFromEventMeta } from '../../../flyout/url/expandable_flyout_state_from_event_meta'; +import { expandableFlyoutStateFromEventMeta } from '../../../flyout/shared/hooks/url/expandable_flyout_state_from_event_meta'; export interface ResolveFlyoutParamsConfig { index: string; diff --git a/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx index 5d0ff73569bd..26f7eb6a8af1 100644 --- a/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx @@ -67,7 +67,7 @@ import { AlertCountByRuleByStatus } from '../../../../common/components/alert_co import { useLicense } from '../../../../common/hooks/use_license'; import { ResponderActionButton } from '../../../../detections/components/endpoint_responder/responder_action_button'; -const ES_HOST_FIELD = 'host.hostname'; +const ES_HOST_FIELD = 'host.name'; const HostOverviewManage = manageQuery(HostOverview); const HostDetailsComponent: React.FC = ({ detailName, hostDetailsPagePath }) => { diff --git a/x-pack/plugins/security_solution/public/flyout/index.tsx b/x-pack/plugins/security_solution/public/flyout/index.tsx index c69deac1030e..81ae4ca0e1e6 100644 --- a/x-pack/plugins/security_solution/public/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/index.tsx @@ -5,20 +5,28 @@ * 2.0. */ -import React from 'react'; -import type { ExpandableFlyoutProps } from '@kbn/expandable-flyout'; +import React, { memo, type FC } from 'react'; +import { + ExpandableFlyout, + type ExpandableFlyoutProps, + ExpandableFlyoutProvider, +} from '@kbn/expandable-flyout'; import type { RightPanelProps } from './right'; import { RightPanel, RightPanelKey } from './right'; import { RightPanelProvider } from './right/context'; import type { LeftPanelProps } from './left'; import { LeftPanel, LeftPanelKey } from './left'; import { LeftPanelProvider } from './left/context'; +import { + SecuritySolutionFlyoutUrlSyncProvider, + useSecurityFlyoutUrlSync, +} from './shared/context/url_sync'; /** * List of all panels that will be used within the document details expandable flyout. * This needs to be passed to the expandable flyout registeredPanels property. */ -export const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels'] = [ +const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels'] = [ { key: RightPanelKey, component: (props) => ( @@ -36,3 +44,42 @@ export const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredP ), }, ]; + +const OuterProviders: FC = ({ children }) => { + return {children}; +}; + +const InnerProviders: FC = ({ children }) => { + const [flyoutRef, handleFlyoutChangedOrClosed] = useSecurityFlyoutUrlSync(); + + return ( + + {children} + + ); +}; + +export const SecuritySolutionFlyoutContextProvider: FC = ({ children }) => ( + + {children} + +); + +SecuritySolutionFlyoutContextProvider.displayName = 'SecuritySolutionFlyoutContextProvider'; + +export const SecuritySolutionFlyout = memo(() => { + const [_flyoutRef, handleFlyoutChangedOrClosed] = useSecurityFlyoutUrlSync(); + + return ( + + ); +}); + +SecuritySolutionFlyout.displayName = 'SecuritySolutionFlyout'; diff --git a/x-pack/plugins/security_solution/public/flyout/left/index.tsx b/x-pack/plugins/security_solution/public/flyout/left/index.tsx index 8c77babd483d..fbba35ae43b5 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/index.tsx @@ -9,7 +9,7 @@ import type { FC } from 'react'; import React, { memo, useMemo } from 'react'; import { useEuiBackgroundColor } from '@elastic/eui'; import { css } from '@emotion/react'; -import type { FlyoutPanel } from '@kbn/expandable-flyout'; +import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; import { PanelHeader } from './header'; import { PanelContent } from './content'; @@ -24,7 +24,7 @@ export const LeftPanelVisualizeTabPath: LeftPanelProps['path'] = ['visualize']; export const LeftPanelInsightsTabPath: LeftPanelProps['path'] = ['insights']; export const LeftPanelInvestigationTabPath: LeftPanelProps['path'] = ['investigation']; -export interface LeftPanelProps extends FlyoutPanel { +export interface LeftPanelProps extends FlyoutPanelProps { key: 'document-details-left'; path?: LeftPanelPaths[]; params?: { diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/share_button.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/share_button.test.tsx index 891bdfacbf17..00b757541d58 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/share_button.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/share_button.test.tsx @@ -9,8 +9,8 @@ import { render, screen, fireEvent } from '@testing-library/react'; import { copyToClipboard } from '@elastic/eui'; import { ShareButton } from './share_button'; import React from 'react'; -import { FLYOUT_URL_PARAM } from '../../url/use_sync_flyout_state_with_url'; import { FLYOUT_HEADER_SHARE_BUTTON_TEST_ID } from './test_ids'; +import { FLYOUT_URL_PARAM } from '../../shared/hooks/url/use_sync_flyout_state_with_url'; jest.mock('@elastic/eui', () => ({ ...jest.requireActual('@elastic/eui'), diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/share_button.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/share_button.tsx index 18a8d71459d2..04405e9b7a31 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/share_button.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/share_button.tsx @@ -8,7 +8,7 @@ import { copyToClipboard, EuiButtonEmpty, EuiCopy } from '@elastic/eui'; import type { FC } from 'react'; import React from 'react'; -import { FLYOUT_URL_PARAM } from '../../url/use_sync_flyout_state_with_url'; +import { FLYOUT_URL_PARAM } from '../../shared/hooks/url/use_sync_flyout_state_with_url'; import { FLYOUT_HEADER_SHARE_BUTTON_TEST_ID } from './test_ids'; import { SHARE } from './translations'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/index.tsx b/x-pack/plugins/security_solution/public/flyout/right/index.tsx index e6f96725412e..f779a2fd98c0 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/index.tsx @@ -7,7 +7,7 @@ import type { FC } from 'react'; import React, { memo, useMemo } from 'react'; -import type { FlyoutPanel } from '@kbn/expandable-flyout'; +import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; import { useRightPanelContext } from './context'; import { PanelHeader } from './header'; @@ -21,7 +21,7 @@ export type RightPanelPaths = 'overview' | 'table' | 'json'; export const RightPanelKey: RightPanelProps['key'] = 'document-details-right'; export const RightPanelTableTabPath: RightPanelProps['path'] = ['table']; -export interface RightPanelProps extends FlyoutPanel { +export interface RightPanelProps extends FlyoutPanelProps { key: 'document-details-right'; path?: RightPanelPaths[]; params?: { diff --git a/x-pack/plugins/security_solution/public/flyout/shared/context/url_sync.tsx b/x-pack/plugins/security_solution/public/flyout/shared/context/url_sync.tsx new file mode 100644 index 000000000000..29b7019fa719 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/shared/context/url_sync.tsx @@ -0,0 +1,47 @@ +/* + * 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, { createContext, useContext, useMemo, type FC } from 'react'; +import { useSyncFlyoutStateWithUrl } from '../hooks/url/use_sync_flyout_state_with_url'; + +export type SecuritySolutionFlyoutCloseContextValue = ReturnType; + +export const SecuritySolutionFlyoutCloseContext = createContext< + SecuritySolutionFlyoutCloseContextValue | undefined +>(undefined); + +/** + * Exposes the flyout close context value (returned from syncUrl) as a hook. + */ +export const useSecurityFlyoutUrlSync = () => { + const contextValue = useContext(SecuritySolutionFlyoutCloseContext); + + if (!contextValue) { + throw new Error('useSecurityFlyoutUrlSync can only be used inside respective provider'); + } + + return contextValue; +}; + +/** + * Provides urlSync hook return value as a context value, for reuse in other components. + * Main goal here is to avoid calling useSyncFlyoutStateWithUrl multiple times. + */ +export const SecuritySolutionFlyoutUrlSyncProvider: FC = ({ children }) => { + const [flyoutRef, handleFlyoutChangedOrClosed] = useSyncFlyoutStateWithUrl(); + + const value: SecuritySolutionFlyoutCloseContextValue = useMemo( + () => [flyoutRef, handleFlyoutChangedOrClosed], + [flyoutRef, handleFlyoutChangedOrClosed] + ); + + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/url/expandable_flyout_state_from_event_meta.ts b/x-pack/plugins/security_solution/public/flyout/shared/hooks/url/expandable_flyout_state_from_event_meta.ts similarity index 95% rename from x-pack/plugins/security_solution/public/flyout/url/expandable_flyout_state_from_event_meta.ts rename to x-pack/plugins/security_solution/public/flyout/shared/hooks/url/expandable_flyout_state_from_event_meta.ts index 1f9a18962092..95e5c509f96d 100644 --- a/x-pack/plugins/security_solution/public/flyout/url/expandable_flyout_state_from_event_meta.ts +++ b/x-pack/plugins/security_solution/public/flyout/shared/hooks/url/expandable_flyout_state_from_event_meta.ts @@ -6,7 +6,7 @@ */ import type { ExpandableFlyoutContext } from '@kbn/expandable-flyout'; -import { RightPanelKey } from '../right'; +import { RightPanelKey } from '../../../right'; interface RedirectParams { index: string; diff --git a/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.test.tsx b/x-pack/plugins/security_solution/public/flyout/shared/hooks/url/use_sync_flyout_state_with_url.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.test.tsx rename to x-pack/plugins/security_solution/public/flyout/shared/hooks/url/use_sync_flyout_state_with_url.test.tsx diff --git a/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.tsx b/x-pack/plugins/security_solution/public/flyout/shared/hooks/url/use_sync_flyout_state_with_url.tsx similarity index 95% rename from x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.tsx rename to x-pack/plugins/security_solution/public/flyout/shared/hooks/url/use_sync_flyout_state_with_url.tsx index 5eaa8e6f1e2c..554cf1d41799 100644 --- a/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.tsx +++ b/x-pack/plugins/security_solution/public/flyout/shared/hooks/url/use_sync_flyout_state_with_url.tsx @@ -9,7 +9,7 @@ import { useCallback, useRef } from 'react'; import type { ExpandableFlyoutApi, ExpandableFlyoutContext } from '@kbn/expandable-flyout'; import { useSyncToUrl } from '@kbn/url-state'; import last from 'lodash/last'; -import { URL_PARAM_KEY } from '../../common/hooks/use_url_state'; +import { URL_PARAM_KEY } from '../../../../common/hooks/use_url_state'; export const FLYOUT_URL_PARAM = URL_PARAM_KEY.eventFlyout; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/mock_data.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/mock_data.ts index 3c5ad1f244b6..27fa20461be1 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/mock_data.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/mock_data.ts @@ -6,12 +6,20 @@ */ import type { RuleAlertsItem, SeverityRuleAlertsAggsResponse } from './use_rule_alerts_items'; +import { + KIBANA_ALERT_SEVERITY, + KIBANA_RULE_ID, + KIBANA_RULE_NAME, + TIMESTAMP, +} from './use_rule_alerts_items'; export const from = '2022-04-05T12:00:00.000Z'; export const to = '2022-04-08T12:00:00.000Z'; export const severityRuleAlertsQuery = { size: 0, + _source: false, + fields: [KIBANA_RULE_NAME, KIBANA_RULE_ID, KIBANA_ALERT_SEVERITY, TIMESTAMP], query: { bool: { filter: [ @@ -62,11 +70,11 @@ export const mockSeverityRuleAlertsResponse: { aggregations: SeverityRuleAlertsA }, hits: [ { - _source: { - 'kibana.alert.rule.name': 'RULE_1', - 'kibana.alert.rule.uuid': '79ec0270-b4c5-11ec-970e-8f7c5a7144f7', - '@timestamp': '2022-04-05T15:58:35.079Z', - 'kibana.alert.severity': 'critical', + fields: { + 'kibana.alert.rule.name': ['RULE_1'], + 'kibana.alert.rule.uuid': ['79ec0270-b4c5-11ec-970e-8f7c5a7144f7'], + '@timestamp': ['2022-04-05T15:58:35.079Z'], + 'kibana.alert.severity': ['critical'], }, }, ], @@ -83,11 +91,11 @@ export const mockSeverityRuleAlertsResponse: { aggregations: SeverityRuleAlertsA }, hits: [ { - _source: { - 'kibana.alert.rule.uuid': '955c79d0-b403-11ec-b5a7-6dc1ed01bdd7', - 'kibana.alert.rule.name': 'RULE_2', - '@timestamp': '2022-04-05T15:58:47.164Z', - 'kibana.alert.severity': 'high', + fields: { + 'kibana.alert.rule.uuid': ['955c79d0-b403-11ec-b5a7-6dc1ed01bdd7'], + 'kibana.alert.rule.name': ['RULE_2'], + '@timestamp': ['2022-04-05T15:58:47.164Z'], + 'kibana.alert.severity': ['high'], }, }, ], @@ -104,11 +112,11 @@ export const mockSeverityRuleAlertsResponse: { aggregations: SeverityRuleAlertsA }, hits: [ { - _source: { - 'kibana.alert.rule.name': 'RULE_3', - 'kibana.alert.rule.uuid': '13bc7bc0-b1d6-11ec-a799-67811b37527a', - '@timestamp': '2022-04-05T15:56:16.606Z', - 'kibana.alert.severity': 'low', + fields: { + 'kibana.alert.rule.name': ['RULE_3'], + 'kibana.alert.rule.uuid': ['13bc7bc0-b1d6-11ec-a799-67811b37527a'], + '@timestamp': ['2022-04-05T15:56:16.606Z'], + 'kibana.alert.severity': ['low'], }, }, ], diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/use_rule_alerts_items.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/use_rule_alerts_items.ts index 1340297f241e..169ba33790ee 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/use_rule_alerts_items.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/use_rule_alerts_items.ts @@ -12,6 +12,7 @@ import { useQueryAlerts } from '../../../../detections/containers/detection_engi import { ALERTS_QUERY_NAMES } from '../../../../detections/containers/detection_engine/alerts/constants'; import { useQueryInspector } from '../../../../common/components/page/manage_query'; import type { ESBoolQuery } from '../../../../../common/typed_json'; +import { firstNonNullValue } from '../../../../../common/endpoint/models/ecs_safety_helpers'; // Formatted item result export interface RuleAlertsItem { @@ -35,11 +36,11 @@ export interface SeverityRuleAlertsAggsResponse { }; hits: [ { - _source: { - '@timestamp': string; - 'kibana.alert.rule.name': string; - 'kibana.alert.rule.uuid': string; - 'kibana.alert.severity': Severity; + fields: { + '@timestamp': string[]; + 'kibana.alert.rule.name': string[]; + 'kibana.alert.rule.uuid': string[]; + 'kibana.alert.severity': Severity[]; }; } ]; @@ -48,6 +49,10 @@ export interface SeverityRuleAlertsAggsResponse { }>; }; } +export const KIBANA_RULE_NAME = 'kibana.alert.rule.name'; +export const KIBANA_RULE_ID = 'kibana.alert.rule.uuid'; +export const KIBANA_ALERT_SEVERITY = 'kibana.alert.severity'; +export const TIMESTAMP = '@timestamp'; const getSeverityRuleAlertsQuery = ({ from, @@ -58,6 +63,8 @@ const getSeverityRuleAlertsQuery = ({ to: string; filterQuery?: ESBoolQuery; }) => ({ + _source: false, + fields: [KIBANA_RULE_NAME, KIBANA_RULE_ID, KIBANA_ALERT_SEVERITY, TIMESTAMP], size: 0, query: { bool: { @@ -101,14 +108,13 @@ const getRuleAlertsItemsFromAggs = ( ): RuleAlertsItem[] => { const buckets = aggregations?.alertsByRule.buckets ?? []; return buckets.map((bucket) => { - const lastAlert = bucket.lastRuleAlert.hits.hits[0]._source; - + const lastAlert = bucket.lastRuleAlert.hits.hits[0].fields; return { - id: lastAlert['kibana.alert.rule.uuid'], + id: firstNonNullValue(lastAlert[KIBANA_RULE_ID]) ?? '', alert_count: bucket.lastRuleAlert.hits.total.value, - name: lastAlert['kibana.alert.rule.name'], - last_alert_at: lastAlert['@timestamp'], - severity: lastAlert['kibana.alert.severity'], + name: firstNonNullValue(lastAlert[KIBANA_RULE_NAME]) ?? '', + last_alert_at: firstNonNullValue(lastAlert[TIMESTAMP]) ?? '', + severity: firstNonNullValue(lastAlert[KIBANA_ALERT_SEVERITY]) ?? 'low', }; }); }; diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/query/index.ts b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/query/index.ts index a73f025878df..c21048fdcfd8 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/query/index.ts +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/query/index.ts @@ -59,9 +59,8 @@ export const getAggregatedAnomaliesQuery = ({ aggs: { entity: { top_hits: { - _source: { - includes: ['host.name', 'user.name'], - }, + _source: false, + fields: ['host.name', 'user.name'], size: 1, }, }, diff --git a/x-pack/plugins/security_solution/scripts/jest.config.js b/x-pack/plugins/security_solution/scripts/jest.config.js new file mode 100644 index 000000000000..dba0d6bc5bad --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/jest.config.js @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/x-pack/plugins/security_solution/scripts'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/scripts', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/scripts/**/*.{ts,tsx}'], +}; diff --git a/x-pack/plugins/security_solution/scripts/junit_transformer/README.md b/x-pack/plugins/security_solution/scripts/junit_transformer/README.md new file mode 100644 index 000000000000..6e8a834da756 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/junit_transformer/README.md @@ -0,0 +1,9 @@ +The failed test reporter creates github issues based on junit reports. Github workflows, and kibanamachine workflows, allow the Kibana Operations team to track and triage flaky tests. These workflows rely on those github issues, specifically their titles, to work. The titles of the github issues contain an encoded version of the file path that contains the failing test. + +This process is facilitated by custom mocha/junit reporters written for the functional test runner and jest. These reporters encode the file name of each spec file and include it in an attribute on elements in the junit report. + +There is no such custom mocha reporter for Cypress, and due to the architecture of Cypress, reusing the existing custom mocha reports, or any of their existing code, is not feasible. Cypress runs in its own process, with its own version of node, and that environment is incompatible with running babel-register. This means we cannot easily interpret the code that implements the existing custom mocha reporters from within Cypress. + +We could compile a library using the code from those custom junit reporters, but there is no established pattern or tooling for doing that. + +For there reasons, our approach is to transform the junit report created by Cypress into a format consumable by the failed test reporter and the kibana operations triage scripts. This script does that. diff --git a/x-pack/plugins/security_solution/scripts/junit_transformer/__snapshots__/junit_transformer.test.ts.snap b/x-pack/plugins/security_solution/scripts/junit_transformer/__snapshots__/junit_transformer.test.ts.snap new file mode 100644 index 000000000000..1474bf9e60c7 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/junit_transformer/__snapshots__/junit_transformer.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`junit_transformer updates the file in place, applying the expected transformation 1`] = ` +" + + + + + + + + AssertionError: Timed out retrying after 150000ms: expected 'http://localhost:5647/app/security/rules/create' to include 'app/security/rules/create1' + at Context.eval (webpack:///./e2e/urls/compatibility.cy.ts:65:13) + + + + + + +" +`; diff --git a/x-pack/plugins/security_solution/scripts/junit_transformer/fixtures/suite_with_failing_test.xml b/x-pack/plugins/security_solution/scripts/junit_transformer/fixtures/suite_with_failing_test.xml new file mode 100644 index 000000000000..9c803eb829ee --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/junit_transformer/fixtures/suite_with_failing_test.xml @@ -0,0 +1,17 @@ + + + + + + + + + AssertionError: Timed out retrying after 150000ms: expected 'http://localhost:5647/app/security/rules/create' to include 'app/security/rules/create1' + at Context.eval (webpack:///./e2e/urls/compatibility.cy.ts:65:13) + + + + + + + \ No newline at end of file diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/index.ts b/x-pack/plugins/security_solution/scripts/junit_transformer/index.js similarity index 76% rename from x-pack/plugins/security_solution/public/common/hooks/eql/index.ts rename to x-pack/plugins/security_solution/scripts/junit_transformer/index.js index 55b0d5dfb296..f1b0280c4ede 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/index.ts +++ b/x-pack/plugins/security_solution/scripts/junit_transformer/index.js @@ -5,4 +5,5 @@ * 2.0. */ -export { useEqlPreview } from './use_eql_preview'; +require('../../../../../src/setup_node_env'); +require('./junit_transformer'); diff --git a/x-pack/plugins/security_solution/scripts/junit_transformer/junit_transformer.test.ts b/x-pack/plugins/security_solution/scripts/junit_transformer/junit_transformer.test.ts new file mode 100644 index 000000000000..e25a349b394a --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/junit_transformer/junit_transformer.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 { promises as fs } from 'fs'; +import { mkdtemp } from 'fs/promises'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import type { CommandArgs } from './lib'; +import { command } from './lib'; + +describe('junit_transformer', () => { + const junitFileName = 'junit.xml'; + let pathPattern: string; + let path: string; + let mockCommandArgs: CommandArgs; + + beforeEach(async () => { + // get a temporary directory + const directory = await mkdtemp(join(tmpdir(), 'junit-transformer-test-')); + + // define a glob pattern that will match the fixture + pathPattern = `${directory}/*`; + + // determine the path for the fixture + path = join(directory, junitFileName); + + // read the fixture and write it to the temporary file + await fs.writeFile( + path, + await fs.readFile(join(__dirname, './fixtures/suite_with_failing_test.xml'), { + encoding: 'utf8', + }) + ); + + mockCommandArgs = { + // define the flags that will be passed to the command + flags: { + pathPattern, + // use the directory as the root directory. This lets us test the relative file path functionality without having a tree of temp files. + rootDirectory: directory, + reportName: 'Test', + writeInPlace: true, + }, + + log: { + info: jest.fn(), + write: jest.fn(), + error: jest.fn(), + success: jest.fn(), + warning: jest.fn(), + }, + }; + }); + it('updates the file in place, applying the expected transformation', async () => { + await command(mockCommandArgs); + expect(await fs.readFile(path, { encoding: 'utf8' })).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/scripts/junit_transformer/junit_transformer.ts b/x-pack/plugins/security_solution/scripts/junit_transformer/junit_transformer.ts new file mode 100644 index 000000000000..1eb29ce1b7dd --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/junit_transformer/junit_transformer.ts @@ -0,0 +1,30 @@ +/* + * 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 { run } from '@kbn/dev-cli-runner'; +import { command } from './lib'; + +/** + * This script processes all junit reports matching a glob pattern. It reads each report, parses it into json, validates that it is a report from Cypress, then transforms the report to a form that can be processed by Kibana Operations workflows and the failed-test-reporter, it then optionally writes the report back, in xml format, to the original file path. + */ +run(command, { + description: ` + Transform junit reports to match the style required by the Kibana Operations flaky test triage workflows such as '/skip'. + `, + flags: { + string: ['pathPattern', 'rootDirectory', 'reportName'], + boolean: ['writeInPlace'], + help: ` + --pathPattern Required, glob passed to globby to select files to operate on + --rootDirectory Required, path of the kibana repo. Used to calcuate the file path of each spec file relative to the Kibana repo + --reportName Required, used as a prefix for the classname. Eventually shows up in the title of flaky test Github issues + --writeInPlace Defaults to false. If passed, rewrite the file in place with transformations. If false, the script will pass the transformed XML as a string to stdout + + If an error is encountered when processing one file, the script will still attempt to process other files. + `, + }, +}); diff --git a/x-pack/plugins/security_solution/scripts/junit_transformer/lib.ts b/x-pack/plugins/security_solution/scripts/junit_transformer/lib.ts new file mode 100644 index 000000000000..d9033c75d485 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/junit_transformer/lib.ts @@ -0,0 +1,254 @@ +/* + * 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. + */ + +/* eslint-disable no-continue */ + +import { createFlagError } from '@kbn/dev-cli-errors'; +import type { RunContext } from '@kbn/dev-cli-runner'; +import { Builder, parseStringPromise } from 'xml2js'; +import { promises as fs } from 'fs'; +import { relative } from 'path'; +import * as t from 'io-ts'; +import { isLeft } from 'fp-ts/lib/Either'; +import { PathReporter } from 'io-ts/lib/PathReporter'; +import globby from 'globby'; + +/** + * Updates the `name` and `classname` attributes of each testcase. + * `name` will have the value of `classname` appended to it. This makes sense because they each contain part of the bdd spec. + * `classname` is replaced with the file path, relative to the kibana project directory, and encoded (by replacing periods with a non-ascii character.) This is the format expected by the failed test reporter and the Kibana Operations flaky test triage workflows. + */ +async function transformedReport({ + reportJson, + specFilePath, + rootDirectory, + reportName, +}: { + reportJson: CypressJunitReport; + specFilePath: string; + rootDirectory: string; + reportName: string; +}): Promise { + for (const testsuite of reportJson.testsuites.testsuite) { + if (!testsuite.testcase) { + // If there are no testcases for this testsuite, skip it + continue; + } + for (const testcase of testsuite.testcase) { + // append the `classname` attribute to the `name` attribute + testcase.$.name = `${testcase.$.name} ${testcase.$.classname}`; + + // calculate the path of the spec file relative to the kibana project directory + const projectRelativePath = relative(rootDirectory, specFilePath); + + // encode the path by relacing dots with a non-ascii character + const encodedPath = projectRelativePath.replace(/\./g, '·'); + + // prepend the encoded path with a report name. This is for display purposes and shows up in the github issue. It is required. Store the value in the `classname` attribute. + testcase.$.classname = `${reportName}.${encodedPath}`; + } + } + + const builder = new Builder(); + // Return the report in an xml string + return builder.buildObject(reportJson); +} + +/** + * Test cases have a name, which is populated with part of the BDD test name, and classname, which is also populated with part of the BDD test name. + */ +const CypressJunitTestCase = t.type({ + $: t.type({ + name: t.string, + classname: t.string, + }), +}); + +/** + * Standard testsuites contain testcase elements, each representing a specific test execution. + */ +const CypressJunitTestSuite = t.intersection([ + t.partial({ + testcase: t.array(CypressJunitTestCase), + }), + t.type({ + $: t.intersection([ + t.type({ + name: t.string, + }), + /* `file` is only found on some suites, namely the 'Root Suite' */ + t.partial({ + file: t.string, + }), + ]), + }), +]); + +const CypressJunitReport = t.type({ + testsuites: t.type({ + testsuite: t.array(CypressJunitTestSuite), + }), +}); + +/** + * This type represents the Cypress-specific flavor of junit report. + **/ +type CypressJunitReport = t.TypeOf; + +/** + * Encapsulate either a successful result, or a recoverable error. This module only throws unrecoverable errors. + */ +type Result = { result: T } | { error: string }; + +/* + * This checks if the junit report contains '·' characters in the classname. This character is used by the kibana operations triage scripts, and the failed test reporter, to replace `.` characters in a path as part of its encoding scheme. If this character is found, we assume that the encoding has already taken place. + */ +function isReportAlreadyProcessed( + report: CypressJunitReport +): { processed: boolean; hadTestCases: true } | { processed: undefined; hadTestCases: false } { + for (const testsuite of report.testsuites.testsuite) { + if (!testsuite.testcase) { + // If there are no testcases for this testsuite, skip it + continue; + } + for (const testcase of testsuite.testcase) { + if (testcase.$.classname.indexOf('·') !== -1) { + return { processed: true, hadTestCases: true }; + } else { + return { processed: false, hadTestCases: true }; + } + } + } + return { processed: undefined, hadTestCases: false }; +} + +/** + * Validate the JSON representation of the Junit XML. + * If there are no errors, this returns `{ result: 'successs' }`, otherwise it returns an error, wrapped in `{ error: string }`. + * + */ +function validatedCypressJunitReport(parsedReport: unknown): Result { + const decoded = CypressJunitReport.decode(parsedReport); + + if (isLeft(decoded)) { + return { + error: `Could not validate data: ${PathReporter.report(decoded).join('\n')}. +`, + }; + } + return { result: decoded.right }; +} + +/** + * Iterate over the test suites and find the root suite, which Cypress populates with the path to the spec file. Return the path. + */ +function findSpecFilePathFromRootSuite(reportJson: CypressJunitReport): Result { + for (const testsuite of reportJson.testsuites.testsuite) { + if (testsuite.$.name === 'Root Suite' && testsuite.$.file) { + return { result: testsuite.$.file }; + } + } + return { + error: "No Root Suite containing a 'file' attribute was found.", + }; +} + +/** + * The CLI command, exported for the sake of automated tests. + */ +export async function command({ flags, log }: CommandArgs) { + if (typeof flags.pathPattern !== 'string' || flags.pathPattern.length === 0) { + throw createFlagError('please provide a single --pathPattern flag'); + } + + if (typeof flags.rootDirectory !== 'string' || flags.rootDirectory.length === 0) { + throw createFlagError('please provide a single --rootDirectory flag'); + } + + if (typeof flags.reportName !== 'string' || flags.reportName.length === 0) { + throw createFlagError('please provide a single --reportName flag'); + } + + for (const path of await globby(flags.pathPattern)) { + // Read the file + const source: string = await fs.readFile(path, 'utf8'); + + // Parse it from XML to json + const unvalidatedReportJson: unknown = await parseStringPromise(source); + + // Apply validation and return the validated report, or an error message + const maybeValidationResult: Result = + validatedCypressJunitReport(unvalidatedReportJson); + + const boilerplate = `This script validates each Junit report to ensure that it was produced by Cypress and that it has not already been processed by this script +This script relies on various assumptions. If your junit report is valid, then you must enhance this script in order to have support for it. If you are not trying to transform a Cypress junit report into a report that is compatible with Kibana Operations workflows, then you are running this script in error.`; + + const logError = (error: string) => { + log.error(`Error while validating ${path}: ${error} +${boilerplate} +`); + }; + + if ('error' in maybeValidationResult) { + logError(maybeValidationResult.error); + // If there is an error, continue trying to process other files. + continue; + } + + const reportJson: CypressJunitReport = maybeValidationResult.result; + + const { processed, hadTestCases } = isReportAlreadyProcessed(reportJson); + if (hadTestCases === false) { + log.warning(`${path} had no test cases. Skipping it. +${boilerplate} +`); + // If there is an error, continue trying to process other files. + continue; + } + + if (processed) { + logError( + "This report appears to have already been transformed because a '·' character was found in the classname. If your test intentionally includes this character as part of its name, remove it. This character is reserved for encoding file paths in the classname attribute." + ); + // If there is an error, continue trying to process other files. + continue; + } + + const maybeSpecFilePath: Result = findSpecFilePathFromRootSuite(reportJson); + + if ('error' in maybeSpecFilePath) { + logError(maybeSpecFilePath.error); + // If there is an error, continue trying to process other files. + continue; + } + + const reportString: string = await transformedReport({ + reportJson, + specFilePath: maybeSpecFilePath.result, + reportName: flags.reportName, + rootDirectory: flags.rootDirectory, + }); + + // If the writeInPlace flag was passed, overwrite the original file, otherwise log the output to stdout + if (flags.writeInPlace) { + log.info(`Wrote transformed junit report to ${path}`); + await fs.writeFile(path, reportString); + } else { + log.write(reportString); + } + } + log.success('task complete'); +} + +/** + * The args passed to our command. These are a subset of RunContext. By using a subset, we make mocking easier. + */ +export interface CommandArgs { + flags: { [index: string]: string | boolean | string[] | undefined }; + /** just pick the parts of `log` that we use. This makes mocking much easier. */ + log: Pick; +} diff --git a/x-pack/plugins/security_solution/server/lib/tags/saved_objects/create_tag.ts b/x-pack/plugins/security_solution/server/lib/tags/saved_objects/create_tag.ts index 5cd1715fb4b9..fac8e22737f1 100644 --- a/x-pack/plugins/security_solution/server/lib/tags/saved_objects/create_tag.ts +++ b/x-pack/plugins/security_solution/server/lib/tags/saved_objects/create_tag.ts @@ -11,6 +11,7 @@ import type { SavedObjectsClientContract, } from '@kbn/core/server'; import type { TagAttributes } from '@kbn/saved-objects-tagging-plugin/common'; +import { getRandomColor } from '../../../../common/utils/get_ramdom_color'; interface CreateTagParams { savedObjectsClient: SavedObjectsClientContract; @@ -20,13 +21,6 @@ interface CreateTagParams { references?: SavedObjectReference[]; } -/** - * Returns the hex representation of a random color (e.g `#F1B7E2`) - */ -const getRandomColor = (): string => { - return `#${String(Math.floor(Math.random() * 16777215).toString(16)).padStart(6, '0')}`; -}; - export const createTag = async ({ savedObjectsClient, tagName, diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index f7a7db93ad6e..95c959e47368 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -4001,6 +4001,16 @@ "type": "long" } } + }, + "environments": { + "properties": { + "1d": { + "type": "long", + "_meta": { + "description": "Total number of unique environments within the last day" + } + } + } } } }, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index cefb4ff467f5..8656553f95b0 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2152,6 +2152,12 @@ "data.sessions.management.flyoutTitle": "Inspecter la session de recherche", "data.triggers.applyFilterDescription": "Lorsque le filtre Kibana est appliqué. Peut être un filtre simple ou un filtre de plage.", "data.triggers.applyFilterTitle": "Appliquer le filtre", + "savedSearch.kibana_context.filters.help": "Spécifier des filtres génériques Kibana", + "savedSearch.kibana_context.help": "Met à jour le contexte général de Kibana.", + "savedSearch.kibana_context.q.help": "Spécifier une recherche en texte libre Kibana", + "savedSearch.kibana_context.savedSearchId.help": "Spécifier l'ID de recherche enregistrée à utiliser pour les requêtes et les filtres", + "savedSearch.kibana_context.timeRange.help": "Spécifier le filtre de plage temporelle Kibana", + "savedSearch.legacyURLConflict.errorMessage": "Cette recherche a la même URL qu'un alias hérité. Désactiver l'alias pour résoudre cette erreur : {json}", "dataViews.deprecations.scriptedFieldsMessage": "Vous avez {numberOfIndexPatternsWithScriptedFields} vues de données ({titlesPreview}…) qui utilisent des champs scriptés. Les champs scriptés sont déclassés et seront supprimés à l'avenir. Utilisez plutôt des champs d'exécution.", "dataViews.fetchFieldErrorTitle": "Erreur lors de l'extraction des champs pour la vue de données {title} (ID : {id})", "dataViews.aliasLabel": "Alias", @@ -4991,7 +4997,6 @@ "savedObjectsManagement.view.savedObjectProblemErrorMessage": "Un problème est survenu avec cet objet enregistré.", "savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage": "La recherche enregistrée associée à cet objet n'existe plus.", "savedSearch.legacyURLConflict.errorMessage": "Cette recherche a la même URL qu'un alias hérité. Désactiver l'alias pour résoudre cette erreur : {json}", - "savedSearch.contentManagementType": "Recherche enregistrée", "securitySolutionPackages.dataTable.eventsTab.unit": "{totalCount, plural, =1 {alerte} one {alertes} many {alertes} other {alertes}}", "securitySolutionPackages.dataTable.unit": "{totalCount, plural, =1 {alerte} one {alertes} many {alertes} other {alertes}}", "securitySolutionPackages.ecsDataQualityDashboard.allTab.allFieldsTableTitle": "Tous les champs – {indexName}", @@ -11442,7 +11447,6 @@ "xpack.csp.cspmIntegration.azureOption.tooltipContent": "Bientôt disponible", "xpack.csp.cspmIntegration.gcpOption.benchmarkTitle": "CIS GCP", "xpack.csp.cspmIntegration.gcpOption.nameTitle": "GCP", - "xpack.csp.cspmIntegration.gcpOption.tooltipContent": "Bientôt disponible", "xpack.csp.cspmIntegration.integration.nameTitle": "Gestion du niveau de sécurité du cloud", "xpack.csp.cspmIntegration.integration.shortNameTitle": "CSPM", "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterPrefixTitle": "Afficher tous les résultats pour ", @@ -16371,8 +16375,6 @@ "xpack.fleet.deletePackagePolicy.failureSingleNotificationTitle": "Erreur lors de la suppression de l'intégration \"{id}\"", "xpack.fleet.deletePackagePolicy.fatalErrorNotificationTitle": "Erreur lors de la suppression de l'intégration", "xpack.fleet.deletePackagePolicy.successSingleNotificationTitle": "L'intégration \"{id}\" a été supprimée", - "xpack.fleet.disabledSecurityDescription": "Vous devez activer Security dans Kibana et Elasticsearch pour utiliser Elastic Fleet.", - "xpack.fleet.disabledSecurityTitle": "La fonctionnalité Security n'est pas activée", "xpack.fleet.editAgentPolicy.cancelButtonText": "Annuler", "xpack.fleet.editAgentPolicy.devtoolsRequestDescription": "Cette requête Kibana met à jour une politique d'agent.", "xpack.fleet.editAgentPolicy.errorNotificationTitle": "Impossible de mettre à jour la stratégie d'agent", @@ -16665,8 +16667,6 @@ "xpack.fleet.integrations.updatePackage.upgradePoliciesCheckboxLabel": "Mettre à niveau les stratégies d'intégration", "xpack.fleet.integrationsAppTitle": "Intégrations", "xpack.fleet.integrationsHeaderTitle": "Intégrations", - "xpack.fleet.invalidLicenseDescription": "Votre licence actuelle a expiré. Les agents Beats enregistrés continuent à fonctionner, mais vous avez besoin d'une licence valide pour accéder à l'interface d'Elastic Fleet.", - "xpack.fleet.invalidLicenseTitle": "Licence expirée", "xpack.fleet.multiRowInput.addAnotherUrl": "Ajouter une autre URL", "xpack.fleet.multiRowInput.addRow": "Ajouter une ligne", "xpack.fleet.multiRowInput.deleteButton": "Supprimer la ligne", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d2dbfe186ec1..41a4a389bd60 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2166,6 +2166,12 @@ "data.sessions.management.flyoutTitle": "検索セッションの検査", "data.triggers.applyFilterDescription": "Kibanaフィルターが適用されるとき。単一の値または範囲フィルターにすることができます。", "data.triggers.applyFilterTitle": "フィルターを適用", + "savedSearch.kibana_context.filters.help": "Kibana ジェネリックフィルターを指定します", + "savedSearch.kibana_context.help": "Kibana グローバルコンテキストを更新します", + "savedSearch.kibana_context.q.help": "自由形式の Kibana テキストクエリを指定します", + "savedSearch.kibana_context.savedSearchId.help": "クエリとフィルターに使用する保存検索ID を指定します。", + "savedSearch.kibana_context.timeRange.help": "Kibana 時間範囲フィルターを指定します", + "savedSearch.legacyURLConflict.errorMessage": "この検索にはレガシーエイリアスと同じURLがあります。このエラーを解決するには、エイリアスを無効にしてください:{json}", "dataViews.deprecations.scriptedFieldsMessage": "スクリプト化されたフィールドを使用する{numberOfIndexPatternsWithScriptedFields}データビュー({titlesPreview}...)があります。スクリプト化されたフィールドは廃止予定であり、今後は削除されます。ランタイムフィールドを使用してください。", "dataViews.fetchFieldErrorTitle": "データビューのフィールド取得中にエラーが発生 {title}(ID:{id})", "dataViews.aliasLabel": "エイリアス", @@ -11456,7 +11462,6 @@ "xpack.csp.cspmIntegration.azureOption.tooltipContent": "まもなくリリース", "xpack.csp.cspmIntegration.gcpOption.benchmarkTitle": "CIS GCP", "xpack.csp.cspmIntegration.gcpOption.nameTitle": "GCP", - "xpack.csp.cspmIntegration.gcpOption.tooltipContent": "まもなくリリース", "xpack.csp.cspmIntegration.integration.nameTitle": "クラウドセキュリティ態勢管理", "xpack.csp.cspmIntegration.integration.shortNameTitle": "CSPM", "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterPrefixTitle": "すべての調査結果を表示 ", @@ -16384,8 +16389,6 @@ "xpack.fleet.deletePackagePolicy.failureSingleNotificationTitle": "統合「{id}」の削除エラー", "xpack.fleet.deletePackagePolicy.fatalErrorNotificationTitle": "統合の削除エラー", "xpack.fleet.deletePackagePolicy.successSingleNotificationTitle": "統合「{id}」を削除しました", - "xpack.fleet.disabledSecurityDescription": "Elastic Fleet を使用するには、Kibana と Elasticsearch でセキュリティを有効にする必要があります。", - "xpack.fleet.disabledSecurityTitle": "セキュリティが有効ではありません", "xpack.fleet.editAgentPolicy.cancelButtonText": "キャンセル", "xpack.fleet.editAgentPolicy.devtoolsRequestDescription": "このKibanaリクエストにより、エージェントポリシーが更新されます。", "xpack.fleet.editAgentPolicy.errorNotificationTitle": "エージェントポリシーを更新できません", @@ -16678,8 +16681,6 @@ "xpack.fleet.integrations.updatePackage.upgradePoliciesCheckboxLabel": "統合ポリシーのアップグレード", "xpack.fleet.integrationsAppTitle": "統合", "xpack.fleet.integrationsHeaderTitle": "統合", - "xpack.fleet.invalidLicenseDescription": "現在のライセンスは期限切れです。登録されたビートエージェントは引き続き動作しますが、Elastic Fleetインターフェースにアクセスするには有効なライセンスが必要です。", - "xpack.fleet.invalidLicenseTitle": "ライセンスの期限切れ", "xpack.fleet.multiRowInput.addAnotherUrl": "別のURLを追加", "xpack.fleet.multiRowInput.addRow": "行の追加", "xpack.fleet.multiRowInput.deleteButton": "行の削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 48ee71935798..e901fddb0b54 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2166,6 +2166,12 @@ "data.sessions.management.flyoutTitle": "检查搜索会话", "data.triggers.applyFilterDescription": "应用 kibana 筛选时。可能是单个值或范围筛选。", "data.triggers.applyFilterTitle": "应用筛选", + "savedSearch.kibana_context.filters.help": "指定 Kibana 常规筛选", + "savedSearch.kibana_context.help": "更新 kibana 全局上下文", + "savedSearch.kibana_context.q.help": "指定 Kibana 自由格式文本查询", + "savedSearch.kibana_context.savedSearchId.help": "指定要用于查询和筛选的已保存搜索 ID", + "savedSearch.kibana_context.timeRange.help": "指定 Kibana 时间范围筛选", + "savedSearch.legacyURLConflict.errorMessage": "此搜索具有与旧版别名相同的 URL。请禁用别名以解决此错误:{json}", "dataViews.deprecations.scriptedFieldsMessage": "您具有 {numberOfIndexPatternsWithScriptedFields} 个使用脚本字段的数据视图 ({titlesPreview}...)。脚本字段已过时,将在未来移除。请改为使用运行时脚本。", "dataViews.fetchFieldErrorTitle": "提取数据视图 {title}(ID:{id})的字段时出错", "dataViews.aliasLabel": "别名", @@ -5004,7 +5010,6 @@ "savedObjectsManagement.view.indexPatternDoesNotExistErrorMessage": "与此对象关联的数据视图不再存在。", "savedObjectsManagement.view.savedObjectProblemErrorMessage": "此已保存对象有问题", "savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage": "与此对象关联的已保存搜索已不存在。", - "savedSearch.legacyURLConflict.errorMessage": "此搜索具有与旧版别名相同的 URL。请禁用别名以解决此错误:{json}", "savedSearch.contentManagementType": "已保存搜索", "securitySolutionPackages.dataTable.eventsTab.unit": "{totalCount, plural, =1 {告警} other {告警}}", "securitySolutionPackages.dataTable.unit": "{totalCount, plural, =1 {告警} other {告警}}", @@ -11456,7 +11461,6 @@ "xpack.csp.cspmIntegration.azureOption.tooltipContent": "即将推出", "xpack.csp.cspmIntegration.gcpOption.benchmarkTitle": "CIS GCP", "xpack.csp.cspmIntegration.gcpOption.nameTitle": "GCP", - "xpack.csp.cspmIntegration.gcpOption.tooltipContent": "即将推出", "xpack.csp.cspmIntegration.integration.nameTitle": "云安全态势管理", "xpack.csp.cspmIntegration.integration.shortNameTitle": "CSPM", "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterPrefixTitle": "显示以下所有结果 ", @@ -16384,8 +16388,6 @@ "xpack.fleet.deletePackagePolicy.failureSingleNotificationTitle": "删除集成“{id}”时出错", "xpack.fleet.deletePackagePolicy.fatalErrorNotificationTitle": "删除集成时出错", "xpack.fleet.deletePackagePolicy.successSingleNotificationTitle": "已删除集成“{id}”", - "xpack.fleet.disabledSecurityDescription": "必须在 Kibana 和 Elasticsearch 启用安全性,才能使用 Elastic Fleet。", - "xpack.fleet.disabledSecurityTitle": "安全性未启用", "xpack.fleet.editAgentPolicy.cancelButtonText": "取消", "xpack.fleet.editAgentPolicy.devtoolsRequestDescription": "此 Kibana 请求将更新代理策略。", "xpack.fleet.editAgentPolicy.errorNotificationTitle": "无法更新代理策略", @@ -16678,8 +16680,6 @@ "xpack.fleet.integrations.updatePackage.upgradePoliciesCheckboxLabel": "升级集成策略", "xpack.fleet.integrationsAppTitle": "集成", "xpack.fleet.integrationsHeaderTitle": "集成", - "xpack.fleet.invalidLicenseDescription": "您当前的许可证已过期。已注册 Beats 代理将继续工作,但您需要有效的许可证,才能访问 Elastic Fleet 界面。", - "xpack.fleet.invalidLicenseTitle": "已过期许可证", "xpack.fleet.multiRowInput.addAnotherUrl": "添加另一个 URL", "xpack.fleet.multiRowInput.addRow": "添加行", "xpack.fleet.multiRowInput.deleteButton": "删除行", diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index e7a6a68f7518..47d3f93fd8dd 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -21,6 +21,64 @@ export default function (providerContext: FtrProviderContext) { describe('fleet_agent_policies', () => { skipIfNoDockerRegistry(providerContext); + + describe('GET /api/fleet/agent_policies', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); + await kibanaServer.savedObjects.cleanStandardList(); + }); + setupFleetAndAgents(providerContext); + + it('should get list agent policies', async () => { + await supertest.get(`/api/fleet/agent_policies`).expect(200); + }); + + it('should get a list of agent policies by kuery', async () => { + await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'TEST', + namespace: 'default', + }) + .expect(200); + const { body: responseBody } = await supertest + .get(`/api/fleet/agent_policies?kuery=ingest-agent-policies.name:TEST`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + expect(responseBody.items.length).to.eql(1); + }); + + it('should return 200 even if the passed kuery does not have prefix ingest-agent-policies', async () => { + await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'TEST-1', + namespace: 'default', + }) + .expect(200); + await supertest + .get(`/api/fleet/agent_policies?kuery=name:TEST-1`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should return 400 if passed kuery is not correct', async () => { + await supertest + .get(`/api/fleet/agent_policies?kuery=ingest-agent-policies.non_existent_parameter:test`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return 400 if passed kuery is invalid', async () => { + await supertest + .get(`/api/fleet/agent_policies?kuery='test%3A'`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + }); + describe('POST /api/fleet/agent_policies', () => { let systemPkgVersion: string; before(async () => { diff --git a/x-pack/test/fleet_api_integration/apis/agents/list.ts b/x-pack/test/fleet_api_integration/apis/agents/list.ts index d051cf677437..328e4a240e77 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/list.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/list.ts @@ -72,15 +72,29 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.total).to.eql(4); }); - it('should return a 400 when given an invalid "kuery" value', async () => { - await supertest.get(`/api/fleet/agents?kuery=.test%3A`).expect(400); + it('should return 200 if the passed kuery is valid', async () => { + await supertest + .get(`/api/fleet/agent_status?kuery=fleet-agents.local_metadata.host.hostname:test`) + .set('kbn-xsrf', 'xxxx') + .expect(200); }); - it('should return a 200 and an empty list when given a "kuery" value with a missing saved object type', async () => { - const { body: apiResponse } = await supertest - .get(`/api/fleet/agents?kuery=m`) // missing saved object type + it('should return 200 also if the passed kuery does not have prefix fleet-agents', async () => { + await supertest + .get(`/api/fleet/agent_status?kuery=local_metadata.host.hostname:test`) + .set('kbn-xsrf', 'xxxx') .expect(200); - expect(apiResponse.total).to.eql(0); + }); + + it('should return a 400 when given an invalid "kuery" value', async () => { + await supertest.get(`/api/fleet/agents?kuery='test%3A'`).expect(400); + }); + + it('should return 400 if passed kuery has non existing parameters', async () => { + await supertest + .get(`/api/fleet/agents?kuery=fleet-agents.non_existent_parameter:healthy`) + .set('kbn-xsrf', 'xxxx') + .expect(400); }); it('should accept a valid "kuery" value', async () => { diff --git a/x-pack/test/fleet_api_integration/apis/agents/status.ts b/x-pack/test/fleet_api_integration/apis/agents/status.ts index 4dbd144493c4..498fbe7c42bc 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/status.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/status.ts @@ -307,5 +307,37 @@ export default function ({ getService }: FtrProviderContext) { }, }); }); + + it('should get a list of agent policies by kuery', async () => { + await supertest + .get(`/api/fleet/agent_status?kuery=fleet-agents.status:healthy`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'TEST', + namespace: 'default', + }) + .expect(200); + }); + + it('should return 200 also if the kuery does not have prefix fleet-agents', async () => { + await supertest + .get(`/api/fleet/agent_status?kuery=status:unhealthy`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should return 400 if passed kuery has non existing parameters', async () => { + await supertest + .get(`/api/fleet/agent_status?kuery=fleet-agents.non_existent_parameter:healthy`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return 400 if passed kuery is not correct', async () => { + await supertest + .get(`/api/fleet/agent_status?kuery='test%3A'`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts b/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts index 05c1a1f8ae6c..197e4da7429b 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts @@ -111,7 +111,7 @@ export default function (providerContext: FtrProviderContext) { const verifyActionResult = async () => { const { body } = await supertest - .get(`/api/fleet/agents?kuery=tags:newTag`) + .get(`/api/fleet/agents?kuery=fleet-agents.tags:newTag`) .set('kbn-xsrf', 'xxx'); expect(body.total).to.eql(4); }; @@ -134,7 +134,7 @@ export default function (providerContext: FtrProviderContext) { const verifyActionResult = async () => { const { body } = await supertest - .get(`/api/fleet/agents?kuery=tags:existingTag`) + .get(`/api/fleet/agents?kuery=fleet-agents.tags:existingTag`) .set('kbn-xsrf', 'xxx'); expect(body.total).to.eql(0); }; @@ -142,7 +142,35 @@ export default function (providerContext: FtrProviderContext) { await pollResult(actionId, 2, verifyActionResult); }); - it('should return a 403 if user lacks fleet all permissions', async () => { + it('should return 200 also if the kuery is valid', async () => { + await supertest + .get(`/api/fleet/agents?kuery=tags:fleet-agents.existingTag`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should return 200 also if the kuery does not have prefix fleet-agents', async () => { + await supertest + .get(`/api/fleet/agents?kuery=tags:existingTag`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should return 400 if the passed kuery is not correct', async () => { + await supertest + .get(`/api/fleet/agents?kuery=fleet-agents.non_existent_parameter:existingTag`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return 400 if the passed kuery is invalid', async () => { + await supertest + .get(`/api/fleet/agents?kuery='test%3A'`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return a 403 if user lacks "fleet all" permissions', async () => { await supertestWithoutAuth .post(`/api/fleet/agents/bulk_update_agent_tags`) .auth(testUsers.fleet_no_access.username, testUsers.fleet_no_access.password) diff --git a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts index 703621dc7f9d..d717c6e285c0 100644 --- a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts @@ -58,6 +58,36 @@ export default function (providerContext: FtrProviderContext) { .auth(testUsers.integr_all_only.username, testUsers.integr_all_only.password) .expect(403); }); + + it('should return 200 if the passed kuery is correct', async () => { + await supertest + .get(`/api/fleet/enrollment_api_keys?kuery=fleet-enrollment-api-keys.policy_id:policy1`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should return 200 if the passed kuery does not have prefix fleet-enrollment-api-keys', async () => { + await supertest + .get(`/api/fleet/enrollment_api_keys?kuery=policy_id:policy1`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should return 400 if the passed kuery is not correct', async () => { + await supertest + .get( + `/api/fleet/enrollment_api_keys?kuery=fleet-enrollment-api-keys.non_existent_parameter:test` + ) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return 400 if the passed kuery is invalid', async () => { + await supertest + .get(`/api/fleet/enrollment_api_keys?kuery='test%3A'`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); }); describe('GET /fleet/enrollment_api_keys/{id}', async () => { diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/get.ts b/x-pack/test/fleet_api_integration/apis/package_policy/get.ts index 06d356c03b37..ce0a7a1f219c 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/get.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/get.ts @@ -426,5 +426,94 @@ export default function (providerContext: FtrProviderContext) { expect(response.body.items[0].id).to.eql(packagePolicyId); }); }); + + describe('get by kuery', async function () { + let agentPolicyId: string; + let endpointPackagePolicyId: string; + + before(async function () { + if (!server.enabled) { + return; + } + + const { body: agentPolicyResponse } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test policy', + namespace: 'default', + }); + agentPolicyId = agentPolicyResponse.item.id; + + const { body: endpointPackagePolicyResponse } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'endpoint-1', + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + inputs: [], + force: true, + package: { + name: 'endpoint', + title: 'Elastic Defend', + version: '8.6.1', + }, + }); + endpointPackagePolicyId = endpointPackagePolicyResponse.item.id; + }); + + after(async function () { + if (!server.enabled) { + return; + } + + await supertest + .post(`/api/fleet/package_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ packagePolicyIds: [endpointPackagePolicyId] }) + .expect(200); + + // uninstall endpoint package + await supertest + .delete(`/api/fleet/epm/packages/endpoint-8.6.1`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + + it('should return 200 if the passed kuery is correct', async () => { + const { body: packagePolicyResponse } = await supertest + .get(`/api/fleet/package_policies?kuery=ingest-package-policies.package.name:endpoint`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + + expect(packagePolicyResponse.items[0].id).to.eql(endpointPackagePolicyId); + }); + it('should return 400 if the passed kuery does not have prefix ingest-package-policies', async () => { + await supertest + .get(`/api/fleet/package_policies?kuery=package.name:endpoint`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return 400 if the passed kuery is not correct', async () => { + await supertest + .get( + `/api/fleet/package_policies?kuery=ingest-package-policies.non_existent_parameter:test` + ) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return 400 if the passed kuery is invalid', async () => { + await supertest + .get(`/api/fleet/package_policies?kuery='test%3A'`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + }); }); } diff --git a/x-pack/test/functional/apps/lens/group2/field_formatters.ts b/x-pack/test/functional/apps/lens/group2/field_formatters.ts index 4116de53e498..4b66436fc1c2 100644 --- a/x-pack/test/functional/apps/lens/group2/field_formatters.ts +++ b/x-pack/test/functional/apps/lens/group2/field_formatters.ts @@ -36,7 +36,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await fieldEditor.confirmDelete(); await PageObjects.lens.waitForFieldMissing('runtimefield'); }); - it('should display url formatter correctly', async () => { await retry.try(async () => { await PageObjects.lens.clickAddField(); @@ -196,5 +195,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('572,732.21%'); }); }); + describe('formatter order', () => { + before(async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + }); + + after(async () => { + await PageObjects.lens.clickField('runtimefield'); + await PageObjects.lens.removeField('runtimefield'); + await fieldEditor.confirmDelete(); + await PageObjects.lens.waitForFieldMissing('runtimefield'); + }); + it('should be overridden by Lens formatter', async () => { + await retry.try(async () => { + await PageObjects.lens.clickAddField(); + await fieldEditor.setName('runtimefield'); + await fieldEditor.setFieldType('long'); + await fieldEditor.enableValue(); + await fieldEditor.typeScript("emit(doc['bytes'].value)"); + await fieldEditor.setFormat(FIELD_FORMAT_IDS.BYTES); + await fieldEditor.save(); + await fieldEditor.waitUntilClosed(); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_metrics > lns-empty-dimension', + operation: 'average', + field: 'runtimefield', + keepOpen: true, + }); + await PageObjects.lens.editDimensionFormat('Bits (1000)', { decimals: 3, prefix: 'blah' }); + await PageObjects.lens.closeDimensionEditor(); + await PageObjects.lens.waitForVisualization(); + expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('5.727kbitblah'); + }); + }); }); } diff --git a/x-pack/test/functional/apps/transform/creation/index_pattern/continuous_transform.ts b/x-pack/test/functional/apps/transform/creation/index_pattern/continuous_transform.ts index 9e3538d036cd..648f7f8ccc04 100644 --- a/x-pack/test/functional/apps/transform/creation/index_pattern/continuous_transform.ts +++ b/x-pack/test/functional/apps/transform/creation/index_pattern/continuous_transform.ts @@ -224,7 +224,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]; for (const testData of testDataList) { - describe(`${testData.suiteTitle}`, function () { + // FLAKY: https://github.com/elastic/kibana/issues/158612 + describe.skip(`${testData.suiteTitle}`, function () { after(async () => { await transform.api.deleteIndices(testData.destinationIndex); await transform.testResources.deleteIndexPatternByTitle(testData.destinationIndex); diff --git a/yarn.lock b/yarn.lock index fc33b1e2ffba..3aafb5c0e781 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1469,10 +1469,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@59.0.0": - version "59.0.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-59.0.0.tgz#c81a82c389011f52ac79e7c59b0551439a0e90b2" - integrity sha512-itEam7dpyHxMNl8eNnZbQa/7M6D741/qsZWjDnXXlirvRsMzpRwhuB0WGxGNXxCuuW/GqWXNlhsqXgKqERS4yw== +"@elastic/charts@59.1.0": + version "59.1.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-59.1.0.tgz#d6b70501f8eec95cc981966713ce5a89028f8fcb" + integrity sha512-qQyDaGEbo/2p+K/hnlobfADmit055kLJbWEkf+rK+uezloNvyBuHETvsoUnrWYG+O5QznHU+wzjSpz7tj1GL3w== dependencies: "@popperjs/core" "^2.4.0" bezier-easing "^2.1.0" @@ -29648,9 +29648,9 @@ wkt-parser@^1.2.4: integrity sha512-A26BOOo7sHAagyxG7iuRhnKMO7Q3mEOiOT4oGUmohtN/Li5wameeU4S6f8vWw6NADTVKljBs8bzA8JPQgSEMVQ== word-wrap@^1.2.3, word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== wordwrap@^1.0.0: version "1.0.0"