From 3497c04d286a53716b2a02bd8f850530b4db8d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 10 Feb 2021 14:18:11 +0000 Subject: [PATCH 01/32] Exclude telemetry schemas from the distributables (#90819) --- src/dev/build/tasks/copy_source_task.ts | 1 + x-pack/tasks/build.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index 5571e0df2a96..06ec40e8fcfa 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -29,6 +29,7 @@ export const CopySource: Task = { '!src/cli/dev.js', '!src/functional_test_runner/**', '!src/dev/**', + '!src/plugins/telemetry/schema/**', // Skip telemetry schemas // this is the dev-only entry '!src/setup_node_env/index.js', '!**/public/**/*.{js,ts,tsx,json}', diff --git a/x-pack/tasks/build.ts b/x-pack/tasks/build.ts index 2cad1de90952..4b6bc2928474 100644 --- a/x-pack/tasks/build.ts +++ b/x-pack/tasks/build.ts @@ -77,6 +77,7 @@ async function copySourceAndBabelify() { '**/public/**/*.{js,ts,tsx,json}', '**/{__tests__,__mocks__,__snapshots__}/**', 'plugins/canvas/shareable_runtime/test/**', + 'plugins/telemetry_collection_xpack/schema/**', // Skip telemetry schemas ], allowEmpty: true, } From a87535624db850e8f9c4aafbc0796ab66d187d3b Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Wed, 10 Feb 2021 15:25:47 +0100 Subject: [PATCH 02/32] [Lens] (performance) replace operationsSupportMatrix by getOperationsByField for drag and drop (#90744) --- .../lens/public/drag_drop/drag_drop.tsx | 99 +++++++++---------- .../dimension_panel/droppable.ts | 40 +++----- .../operations/operations.test.ts | 45 ++++++++- .../operations/operations.ts | 19 ++-- 4 files changed, 115 insertions(+), 88 deletions(-) diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index 898071e85ea7..07c1368e5345 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -119,8 +119,14 @@ interface DragInnerProps extends BaseProps { /** * The props for a non-draggable instance of that component. */ -interface DropInnerProps extends BaseProps, DragContextState { - isDragging: boolean; +interface DropInnerProps extends BaseProps { + dragging: DragContextState['dragging']; + setKeyboardMode: DragContextState['setKeyboardMode']; + setDragging: DragContextState['setDragging']; + setActiveDropTarget: DragContextState['setActiveDropTarget']; + setA11yMessage: DragContextState['setA11yMessage']; + registerDropTarget: DragContextState['registerDropTarget']; + isActiveDropTarget: boolean; isNotDroppable: boolean; } @@ -141,27 +147,42 @@ export const DragDrop = (props: BaseProps) => { const { value, draggable, dropType, reorderableGroup } = props; const isDragging = !!(draggable && value.id === dragging?.id); - const dragProps = { - ...props, - isDragging, - keyboardMode: isDragging ? keyboardMode : false, // optimization to not rerender all dragging components - activeDropTarget: isDragging ? activeDropTarget : undefined, // optimization to not rerender all dragging components - setKeyboardMode, - setDragging, - setActiveDropTarget, - setA11yMessage, - }; + if (draggable && !dropType) { + const dragProps = { + ...props, + isDragging, + keyboardMode: isDragging ? keyboardMode : false, // optimization to not rerender all dragging components + activeDropTarget: isDragging ? activeDropTarget : undefined, // optimization to not rerender all dragging components + setKeyboardMode, + setDragging, + setActiveDropTarget, + setA11yMessage, + }; + if (reorderableGroup && reorderableGroup.length > 1) { + return ( + + ); + } else { + return ; + } + } + const isActiveDropTarget = Boolean( + activeDropTarget?.activeDropTarget && activeDropTarget.activeDropTarget.id === value.id + ); const dropProps = { ...props, setKeyboardMode, - keyboardMode, dragging, setDragging, - activeDropTarget, + isActiveDropTarget, setActiveDropTarget, registerDropTarget, - isDragging, setA11yMessage, isNotDroppable: // If the configuration has provided a droppable flag, but this particular item is not @@ -169,21 +190,6 @@ export const DragDrop = (props: BaseProps) => { // draggable and drop targets !!(!dropType && dragging && value.id !== dragging.id), }; - - if (draggable && !dropType) { - if (reorderableGroup && reorderableGroup.length > 1) { - return ( - - ); - } else { - return ; - } - } if ( reorderableGroup && reorderableGroup.length > 1 && @@ -340,19 +346,16 @@ const DropInner = memo(function DropInner(props: DropInnerProps) { children, draggable, dragging, - isDragging, isNotDroppable, - dragType = 'copy', dropType, - keyboardMode, - activeDropTarget, - registerDropTarget, - setActiveDropTarget, + order, getAdditionalClassesOnEnter, getAdditionalClassesOnDroppable, + isActiveDropTarget, + registerDropTarget, + setActiveDropTarget, setKeyboardMode, setDragging, - order, setA11yMessage, } = props; @@ -365,11 +368,6 @@ const DropInner = memo(function DropInner(props: DropInnerProps) { } }, [order, value, registerDropTarget, dropType]); - const activeDropTargetMatches = - activeDropTarget?.activeDropTarget && activeDropTarget.activeDropTarget.id === value.id; - - const isMoveDragging = isDragging && dragType === 'move'; - const classesOnEnter = getAdditionalClassesOnEnter?.(dropType); const classesOnDroppable = getAdditionalClassesOnDroppable?.(dropType); @@ -377,15 +375,12 @@ const DropInner = memo(function DropInner(props: DropInnerProps) { 'lnsDragDrop', { 'lnsDragDrop-isDraggable': draggable, - 'lnsDragDrop-isDragging': isDragging, - 'lnsDragDrop-isHidden': isMoveDragging && !keyboardMode, 'lnsDragDrop-isDroppable': !draggable, 'lnsDragDrop-isDropTarget': dropType && dropType !== 'reorder', - 'lnsDragDrop-isActiveDropTarget': - dropType && activeDropTargetMatches && dropType !== 'reorder', - 'lnsDragDrop-isNotDroppable': !isMoveDragging && isNotDroppable, + 'lnsDragDrop-isActiveDropTarget': dropType && isActiveDropTarget && dropType !== 'reorder', + 'lnsDragDrop-isNotDroppable': isNotDroppable, }, - classesOnEnter && { [classesOnEnter]: activeDropTargetMatches }, + classesOnEnter && { [classesOnEnter]: isActiveDropTarget }, classesOnDroppable && { [classesOnDroppable]: dropType } ); @@ -396,7 +391,7 @@ const DropInner = memo(function DropInner(props: DropInnerProps) { e.preventDefault(); // An optimization to prevent a bunch of React churn. - if (!activeDropTargetMatches && dragging && onDrop) { + if (!isActiveDropTarget && dragging && onDrop) { setActiveDropTarget({ ...value, dropType, onDrop }); setA11yMessage(announce.selectedTarget(dragging.humanData, value.humanData, dropType)); } @@ -602,7 +597,7 @@ const ReorderableDrop = memo(function ReorderableDrop( dragging, setDragging, setKeyboardMode, - activeDropTarget, + isActiveDropTarget, setActiveDropTarget, reorderableGroup, setA11yMessage, @@ -610,8 +605,6 @@ const ReorderableDrop = memo(function ReorderableDrop( } = props; const currentIndex = reorderableGroup.findIndex((i) => i.id === value.id); - const activeDropTargetMatches = - activeDropTarget?.activeDropTarget && activeDropTarget.activeDropTarget.id === value.id; const { reorderState: { isReorderOn, reorderedItems, draggingHeight, direction }, @@ -646,7 +639,7 @@ const ReorderableDrop = memo(function ReorderableDrop( e.preventDefault(); // An optimization to prevent a bunch of React churn. - if (!activeDropTargetMatches && dropType && onDrop) { + if (!isActiveDropTarget && dropType && onDrop) { setActiveDropTarget({ ...value, dropType, onDrop }); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts index cbd599743f81..69c7e8c3c2ae 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts @@ -12,12 +12,11 @@ import { DraggedOperation, } from '../../types'; import { IndexPatternColumn } from '../indexpattern'; -import { insertOrReplaceColumn, deleteColumn } from '../operations'; +import { insertOrReplaceColumn, deleteColumn, getOperationTypesForField } from '../operations'; import { mergeLayer } from '../state_helpers'; import { hasField, isDraggedField } from '../utils'; import { IndexPatternPrivateState, IndexPatternField, DraggedField } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; -import { getOperationSupportMatrix } from './operation_support'; type DropHandlerProps = DatasourceDimensionDropHandlerProps & { droppedItem: T; @@ -34,7 +33,8 @@ export function getDropTypes( const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; function hasOperationForField(field: IndexPatternField) { - return !!getOperationSupportMatrix(props).operationByField[field.name]; + const operationsForNewField = getOperationTypesForField(field, props.filterOperations); + return !!operationsForNewField.length; } const currentColumn = props.state.layers[props.layerId].columns[props.columnId]; @@ -171,10 +171,9 @@ function onMoveDropToNonCompatibleGroup(props: DropHandlerProps) { const { columnId, setState, state, layerId, droppedItem } = props; - const operationSupportMatrix = getOperationSupportMatrix(props); - function hasOperationForField(field: IndexPatternField) { - return !!operationSupportMatrix.operationByField[field.name]; - } + const operationsForNewField = getOperationTypesForField( + droppedItem.field, + props.filterOperations + ); - if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { + if (!isDraggedField(droppedItem) || !operationsForNewField.length) { // TODO: What do we do if we couldn't find a column? return false; } - // dragged field, not operation - - const operationsForNewField = operationSupportMatrix.operationByField[droppedItem.field.name]; - - if (!operationsForNewField || operationsForNewField.size === 0) { - return false; - } - const layer = state.layers[layerId]; const selectedColumn: IndexPatternColumn | null = layer.columns[columnId] || null; @@ -308,18 +299,13 @@ function onFieldDrop(props: DropHandlerProps) { // Detects if we can change the field only, otherwise change field + operation const fieldIsCompatibleWithCurrent = - selectedColumn && - operationSupportMatrix.operationByField[droppedItem.field.name]?.has( - selectedColumn.operationType - ); + selectedColumn && operationsForNewField.includes(selectedColumn.operationType); const newLayer = insertOrReplaceColumn({ layer, columnId, indexPattern: currentIndexPattern, - op: fieldIsCompatibleWithCurrent - ? selectedColumn.operationType - : operationsForNewField.values().next().value, + op: fieldIsCompatibleWithCurrent ? selectedColumn.operationType : operationsForNewField[0], field: droppedItem.field, }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 4249f8397716..360e1697ae58 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -56,7 +56,22 @@ describe('getOperationTypesForField', () => { aggregatable: true, searchable: true, }) - ).toEqual(expect.arrayContaining(['terms'])); + ).toEqual(['terms', 'cardinality', 'last_value']); + }); + + it('should return only bucketed operations on strings when passed proper filterOperations function', () => { + expect( + getOperationTypesForField( + { + type: 'string', + name: 'a', + displayName: 'aLabel', + aggregatable: true, + searchable: true, + }, + (op) => op.isBucketed + ) + ).toEqual(['terms']); }); it('should return operations on numbers', () => { @@ -68,7 +83,33 @@ describe('getOperationTypesForField', () => { aggregatable: true, searchable: true, }) - ).toEqual(expect.arrayContaining(['avg', 'sum', 'min', 'max'])); + ).toEqual([ + 'range', + 'terms', + 'avg', + 'sum', + 'min', + 'max', + 'cardinality', + 'median', + 'percentile', + 'last_value', + ]); + }); + + it('should return only metric operations on numbers when passed proper filterOperations function', () => { + expect( + getOperationTypesForField( + { + type: 'number', + name: 'a', + displayName: 'aLabel', + aggregatable: true, + searchable: true, + }, + (op) => !op.isBucketed + ) + ).toEqual(['avg', 'sum', 'min', 'max', 'cardinality', 'median', 'percentile', 'last_value']); }); it('should return operations on dates', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts index 2869b14208e1..63671fe35e99 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -58,13 +58,20 @@ export function getSortScoreByPriority( * Returns all `OperationType`s that can build a column using `buildColumn` based on the * passed in field. */ -export function getOperationTypesForField(field: IndexPatternField): OperationType[] { +export function getOperationTypesForField( + field: IndexPatternField, + filterOperations?: (operation: OperationMetadata) => boolean +): OperationType[] { return operationDefinitions - .filter( - (operationDefinition) => - operationDefinition.input === 'field' && - operationDefinition.getPossibleOperationForField(field) - ) + .filter((operationDefinition) => { + if (operationDefinition.input !== 'field') { + return false; + } + const possibleOperation = operationDefinition.getPossibleOperationForField(field); + return filterOperations + ? possibleOperation && filterOperations(possibleOperation) + : possibleOperation; + }) .sort(getSortScoreByPriority) .map(({ type }) => type); } From 40f44a91c8902bcbd460ca6b5dbee740b3754f52 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Wed, 10 Feb 2021 16:08:20 +0100 Subject: [PATCH 03/32] [Discover] Add "Hide chart" / "Show chart" persistence (#88603) --- .../public/application/angular/discover.js | 22 +++++---- .../application/angular/discover_legacy.html | 1 - .../application/angular/discover_state.ts | 4 ++ .../application/components/discover.test.tsx | 5 +- .../application/components/discover.tsx | 32 +++++++++---- .../components/discover_topnav.test.tsx | 11 +++-- .../components/discover_topnav.tsx | 18 ++++--- .../public/application/components/types.ts | 24 ++++++---- .../helpers/persist_saved_search.ts | 3 ++ .../public/saved_searches/_saved_search.ts | 2 + .../discover/public/saved_searches/types.ts | 1 + .../discover/server/saved_objects/search.ts | 1 + .../apps/discover/_discover_histogram.ts | 48 +++++++++++++++++-- 13 files changed, 128 insertions(+), 44 deletions(-) diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index af63485507d0..3733e8669895 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -252,6 +252,12 @@ function discoverController($route, $scope, Promise) { (prop) => !_.isEqual(newStatePartial[prop], oldStatePartial[prop]) ); + if (oldStatePartial.hideChart && !newStatePartial.hideChart) { + // in case the histogram is hidden, no data is requested + // so when changing this state data needs to be fetched + changes.push(true); + } + if (changes.length) { refetch$.next(); } @@ -313,6 +319,8 @@ function discoverController($route, $scope, Promise) { setAppState, data, stateContainer, + searchSessionManager, + refetch$, }; const inspectorAdapters = ($scope.opts.inspectorAdapters = { @@ -412,6 +420,9 @@ function discoverController($route, $scope, Promise) { if (savedSearch.grid) { defaultState.grid = savedSearch.grid; } + if (savedSearch.hideChart) { + defaultState.hideChart = savedSearch.hideChart; + } return defaultState; } @@ -562,13 +573,6 @@ function discoverController($route, $scope, Promise) { }); }; - $scope.handleRefresh = function (_payload, isUpdate) { - if (isUpdate === false) { - searchSessionManager.removeSearchSessionIdFromURL({ replace: false }); - refetch$.next(); - } - }; - function getDimensions(aggs, timeRange) { const [metric, agg] = aggs; agg.params.timeRange = timeRange; @@ -601,7 +605,7 @@ function discoverController($route, $scope, Promise) { function onResults(resp) { inspectorRequest.stats(getResponseInspectorStats(resp, $scope.searchSource)).ok({ json: resp }); - if (getTimeField()) { + if (getTimeField() && !$scope.state.hideChart) { const tabifiedData = tabifyAggResponse($scope.opts.chartAggConfigs, resp); $scope.searchSource.rawResponse = resp; $scope.histogramData = discoverResponseHandler( @@ -704,7 +708,7 @@ function discoverController($route, $scope, Promise) { async function setupVisualization() { // If no timefield has been specified we don't create a histogram of messages - if (!getTimeField()) return; + if (!getTimeField() || $scope.state.hideChart) return; const { interval: histogramInterval } = $scope.state; const visStateAggs = [ diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index dc18b7929318..501496494106 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -17,7 +17,6 @@ state="state" time-range="timeRange" top-nav-menu="topNavMenu" - update-query="handleRefresh" use-new-fields-api="useNewFieldsApi" unmapped-fields-config="unmappedFieldsConfig" > diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index 5e93966d78d9..93fc49b65cbc 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -44,6 +44,10 @@ export interface AppState { * Data Grid related state */ grid?: DiscoverGridSettings; + /** + * Hide chart + */ + hideChart?: boolean; /** * id of the used index pattern */ diff --git a/src/plugins/discover/public/application/components/discover.test.tsx b/src/plugins/discover/public/application/components/discover.test.tsx index f0f11558abd6..00554196e11f 100644 --- a/src/plugins/discover/public/application/components/discover.test.tsx +++ b/src/plugins/discover/public/application/components/discover.test.tsx @@ -25,6 +25,8 @@ import { indexPatternWithTimefieldMock } from '../../__mocks__/index_pattern_wit import { calcFieldCounts } from '../helpers/calc_field_counts'; import { DiscoverProps } from './types'; import { RequestAdapter } from '../../../../inspector/common'; +import { Subject } from 'rxjs'; +import { DiscoverSearchSessionManager } from '../angular/discover_search_session'; const mockNavigation = navigationPluginMock.createStartContract(); @@ -73,8 +75,10 @@ function getProps(indexPattern: IndexPattern): DiscoverProps { indexPatternList: (indexPattern as unknown) as Array>, inspectorAdapters: { requests: {} as RequestAdapter }, navigateTo: jest.fn(), + refetch$: {} as Subject, sampleSize: 10, savedSearch: savedSearchMock, + searchSessionManager: {} as DiscoverSearchSessionManager, setHeaderActionMenu: jest.fn(), timefield: indexPattern.timeFieldName || '', setAppState: jest.fn(), @@ -86,7 +90,6 @@ function getProps(indexPattern: IndexPattern): DiscoverProps { rows: esHits, searchSource: searchSourceMock, state: { columns: [] }, - updateQuery: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index 99baa30e18c7..71650a4a3847 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import './discover.scss'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useState, useRef, useMemo, useCallback } from 'react'; import { EuiButtonEmpty, EuiButtonIcon, @@ -66,7 +66,6 @@ export function Discover({ searchSource, state, timeRange, - updateQuery, unmappedFieldsConfig, }: DiscoverProps) { const [expandedDoc, setExpandedDoc] = useState(undefined); @@ -76,8 +75,11 @@ export function Discover({ // collapse icon isn't displayed in mobile view, use it to detect which view is displayed return collapseIcon && !collapseIcon.current; }; - - const [toggleOn, toggleChart] = useState(true); + const toggleHideChart = useCallback(() => { + const newState = { ...state, hideChart: !state.hideChart }; + opts.stateContainer.setAppState(newState); + }, [state, opts]); + const hideChart = useMemo(() => state.hideChart, [state]); const { savedSearch, indexPatternList, config, services, data, setAppState } = opts; const { trackUiMetric, capabilities, indexPatterns } = services; const [isSidebarClosed, setIsSidebarClosed] = useState(false); @@ -89,6 +91,15 @@ export function Discover({ const contentCentered = resultState === 'uninitialized'; const isLegacy = services.uiSettings.get('doc_table:legacy'); const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); + const updateQuery = useCallback( + (_payload, isUpdate?: boolean) => { + if (isUpdate === false) { + opts.searchSessionManager.removeSearchSessionIdFromURL({ replace: false }); + opts.refetch$.next(); + } + }, + [opts] + ); const { onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useMemo( () => @@ -192,7 +203,8 @@ export function Discover({ indexPattern={indexPattern} opts={opts} onOpenInspector={onOpenInspector} - state={state} + query={state.query} + savedQuery={state.savedQuery} updateQuery={updateQuery} /> @@ -277,7 +289,7 @@ export function Discover({ onResetQuery={resetQuery} /> - {toggleOn && ( + {!hideChart && ( { - toggleChart(!toggleOn); + toggleHideChart(); }} data-test-subj="discoverChartToggle" > - {toggleOn + {!hideChart ? i18n.translate('discover.hideChart', { defaultMessage: 'Hide chart', }) @@ -312,7 +324,7 @@ export function Discover({ {isLegacy && } - {toggleOn && opts.timefield && ( + {!hideChart && opts.timefield && (
>, inspectorAdapters: { requests: {} as RequestAdapter }, navigateTo: jest.fn(), + refetch$: {} as Subject, sampleSize: 10, savedSearch: savedSearchMock, + searchSessionManager: {} as DiscoverSearchSessionManager, services, setAppState: jest.fn(), setHeaderActionMenu: jest.fn(), stateContainer: {} as GetStateReturn, timefield: indexPattern.timeFieldName || '', }, - state, + query: {} as Query, + savedQuery: '', updateQuery: jest.fn(), onOpenInspector: jest.fn(), }; diff --git a/src/plugins/discover/public/application/components/discover_topnav.tsx b/src/plugins/discover/public/application/components/discover_topnav.tsx index 69a1433b6505..fd2aba22aa41 100644 --- a/src/plugins/discover/public/application/components/discover_topnav.tsx +++ b/src/plugins/discover/public/application/components/discover_topnav.tsx @@ -8,17 +8,21 @@ import React, { useMemo } from 'react'; import { DiscoverProps } from './types'; import { getTopNavLinks } from './top_nav/get_top_nav_links'; +import { Query, TimeRange } from '../../../../data/common/query'; -export type DiscoverTopNavProps = Pick< - DiscoverProps, - 'indexPattern' | 'updateQuery' | 'state' | 'opts' -> & { onOpenInspector: () => void }; +export type DiscoverTopNavProps = Pick & { + onOpenInspector: () => void; + query?: Query; + savedQuery?: string; + updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; +}; export const DiscoverTopNav = ({ indexPattern, opts, onOpenInspector, - state, + query, + savedQuery, updateQuery, }: DiscoverTopNavProps) => { const showDatePicker = useMemo(() => indexPattern.isTimeBased(), [indexPattern]); @@ -58,9 +62,9 @@ export const DiscoverTopNav = ({ indexPatterns={[indexPattern]} onQuerySubmit={updateQuery} onSavedQueryIdChange={updateSavedQueryId} - query={state.query} + query={query} setMenuMountPoint={opts.setHeaderActionMenu} - savedQueryId={state.savedQuery} + savedQueryId={savedQuery} screenTitle={opts.savedSearch.title} showDatePicker={showDatePicker} showSaveQuery={!!opts.services.capabilities.discover.saveQuery} diff --git a/src/plugins/discover/public/application/components/types.ts b/src/plugins/discover/public/application/components/types.ts index ee06bcab6528..e276795f9ed7 100644 --- a/src/plugins/discover/public/application/components/types.ts +++ b/src/plugins/discover/public/application/components/types.ts @@ -7,6 +7,7 @@ */ import { IUiSettingsClient, MountPoint, SavedObject } from 'kibana/public'; +import { Subject } from 'rxjs'; import { Chart } from '../angular/helpers/point_series'; import { IndexPattern } from '../../../../data/common/index_patterns/index_patterns'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; @@ -17,13 +18,12 @@ import { FilterManager, IndexPatternAttributes, ISearchSource, - Query, - TimeRange, } from '../../../../data/public'; import { SavedSearch } from '../../saved_searches'; import { AppState, GetStateReturn } from '../angular/discover_state'; import { RequestAdapter } from '../../../../inspector/common'; import { DiscoverServices } from '../../build_services'; +import { DiscoverSearchSessionManager } from '../angular/discover_search_session'; export interface DiscoverProps { /** @@ -97,10 +97,18 @@ export interface DiscoverProps { * List of available index patterns */ indexPatternList: Array>; + /** + * Refetch observable + */ + refetch$: Subject; /** * Kibana core services used by discover */ services: DiscoverServices; + /** + * Helps with state management of search session + */ + searchSessionManager: DiscoverSearchSessionManager; /** * The number of documents that can be displayed in the table/grid */ @@ -113,10 +121,6 @@ export interface DiscoverProps { * Function to set the header menu */ setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; - /** - * Functions for retrieving/mutating state - */ - stateContainer: GetStateReturn; /** * Timefield of the currently used index pattern */ @@ -125,6 +129,10 @@ export interface DiscoverProps { * Function to set the current state */ setAppState: (state: Partial) => void; + /** + * State container providing globalState, appState and functions + */ + stateContainer: GetStateReturn; }; /** * Function to reset the current query @@ -150,10 +158,6 @@ export interface DiscoverProps { * Currently selected time range */ timeRange?: { from: string; to: string }; - /** - * Function to update the actual query - */ - updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; /** * An object containing properties for proper handling of unmapped fields in the UI */ diff --git a/src/plugins/discover/public/application/helpers/persist_saved_search.ts b/src/plugins/discover/public/application/helpers/persist_saved_search.ts index 1bebf60c0a80..06e90c93bc77 100644 --- a/src/plugins/discover/public/application/helpers/persist_saved_search.ts +++ b/src/plugins/discover/public/application/helpers/persist_saved_search.ts @@ -48,6 +48,9 @@ export async function persistSavedSearch( if (state.grid) { savedSearch.grid = state.grid; } + if (state.hideChart) { + savedSearch.hideChart = state.hideChart; + } try { const id = await savedSearch.save(saveOptions); diff --git a/src/plugins/discover/public/saved_searches/_saved_search.ts b/src/plugins/discover/public/saved_searches/_saved_search.ts index d5bd3ea4011b..a7b6ef49cacd 100644 --- a/src/plugins/discover/public/saved_searches/_saved_search.ts +++ b/src/plugins/discover/public/saved_searches/_saved_search.ts @@ -14,6 +14,7 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { public static mapping = { title: 'text', description: 'text', + hideChart: 'boolean', hits: 'integer', columns: 'keyword', grid: 'object', @@ -35,6 +36,7 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { mapping: { title: 'text', description: 'text', + hideChart: 'boolean', hits: 'integer', columns: 'keyword', grid: 'object', diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts index 24fbbcb61cb4..4646744ee0ef 100644 --- a/src/plugins/discover/public/saved_searches/types.ts +++ b/src/plugins/discover/public/saved_searches/types.ts @@ -24,6 +24,7 @@ export interface SavedSearch { lastSavedTitle?: string; copyOnSave?: boolean; pre712?: boolean; + hideChart?: boolean; } export interface SavedSearchLoader { get: (id: string) => Promise; diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index 43f107399ac3..de3a2197fe0a 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -34,6 +34,7 @@ export const searchSavedObjectType: SavedObjectsType = { properties: { columns: { type: 'keyword', index: false, doc_values: false }, description: { type: 'text' }, + hideChart: { type: 'boolean', index: false, doc_values: false }, hits: { type: 'integer', index: false, doc_values: false }, kibanaSavedObjectMeta: { properties: { diff --git a/test/functional/apps/discover/_discover_histogram.ts b/test/functional/apps/discover/_discover_histogram.ts index 56dc784eac70..9a6692dc793d 100644 --- a/test/functional/apps/discover/_discover_histogram.ts +++ b/test/functional/apps/discover/_discover_histogram.ts @@ -19,6 +19,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'long-window-logstash-*', 'dateFormat:tz': 'Europe/Berlin', }; + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); describe('discover histogram', function describeIndexTests() { before(async () => { @@ -35,11 +37,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.testUser.restoreDefaults(); }); - async function prepareTest(fromTime: string, toTime: string, interval: string) { + async function prepareTest(fromTime: string, toTime: string, interval?: string) { await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.discover.waitUntilSearchingHasFinished(); - await PageObjects.discover.setChartInterval(interval); - await PageObjects.header.waitUntilLoadingHasFinished(); + if (interval) { + await PageObjects.discover.setChartInterval(interval); + await PageObjects.header.waitUntilLoadingHasFinished(); + } } it('should visualize monthly data with different day intervals', async () => { @@ -65,5 +69,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const chartIntervalIconTip = await PageObjects.discover.getChartIntervalWarningIcon(); expect(chartIntervalIconTip).to.be(true); }); + it('should allow hide/show histogram, persisted in url state', async () => { + const fromTime = 'Jan 01, 2010 @ 00:00:00.000'; + const toTime = 'Mar 21, 2019 @ 00:00:00.000'; + await prepareTest(fromTime, toTime); + let canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(true); + await testSubjects.click('discoverChartToggle'); + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(false); + // histogram is hidden, when reloading the page it should remain hidden + await browser.refresh(); + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(false); + await testSubjects.click('discoverChartToggle'); + await PageObjects.header.waitUntilLoadingHasFinished(); + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(true); + }); + it('should allow hiding the histogram, persisted in saved search', async () => { + const fromTime = 'Jan 01, 2010 @ 00:00:00.000'; + const toTime = 'Mar 21, 2019 @ 00:00:00.000'; + const savedSearch = 'persisted hidden histogram'; + await prepareTest(fromTime, toTime); + await testSubjects.click('discoverChartToggle'); + let canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(false); + await PageObjects.discover.saveSearch(savedSearch); + await PageObjects.header.waitUntilLoadingHasFinished(); + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(false); + await testSubjects.click('discoverChartToggle'); + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(true); + await PageObjects.discover.clickResetSavedSearchButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); + canvasExists = await elasticChart.canvasExists(); + expect(canvasExists).to.be(false); + }); }); } From 4aabf358b02d9daaf8cc40eb139745f238a34e07 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 10 Feb 2021 09:08:59 -0600 Subject: [PATCH 04/32] [Workplace Search] Fix error message in Schema (#90869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In SchemaErrors, we have an edge case where a user navigates to a non-existent error message state. When migrating I took a stab at passing the custom message to the `flashAPIErrors` helper. It turns out that since we don’t have to parse an error object, we can just set the error message directly --- .../components/schema/schema_logic.test.ts | 12 +++++++----- .../components/schema/schema_logic.ts | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts index af650d95efaf..28850531ebb9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts @@ -35,7 +35,12 @@ import { SchemaLogic, dataTypeOptions } from './schema_logic'; describe('SchemaLogic', () => { const { http } = mockHttpValues; - const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + const { + clearFlashMessages, + flashAPIErrors, + setSuccessMessage, + setErrorMessage, + } = mockFlashMessageHelpers; const { mount } = new LogicMounter(SchemaLogic); const defaultValues = { @@ -298,10 +303,7 @@ describe('SchemaLogic', () => { ); await nextTick(); - expect(flashAPIErrors).toHaveBeenCalledWith({ - error: 'this is an error', - message: SCHEMA_FIELD_ERRORS_ERROR_MESSAGE, - }); + expect(setErrorMessage).toHaveBeenCalledWith(SCHEMA_FIELD_ERRORS_ERROR_MESSAGE); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts index 9906efe707d8..10b7f85a631b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts @@ -13,6 +13,7 @@ import { ADD, UPDATE } from '../../../../../shared/constants/operations'; import { flashAPIErrors, setSuccessMessage, + setErrorMessage, clearFlashMessages, } from '../../../../../shared/flash_messages'; import { HttpLogic } from '../../../../../shared/http'; @@ -295,7 +296,7 @@ export const SchemaLogic = kea>({ fieldCoercionErrors: response.fieldCoercionErrors, }); } catch (e) { - flashAPIErrors({ ...e, message: SCHEMA_FIELD_ERRORS_ERROR_MESSAGE }); + setErrorMessage(SCHEMA_FIELD_ERRORS_ERROR_MESSAGE); } }, addNewField: ({ fieldName, newFieldType }) => { From fa18be9beb49ac8152cc39a0705946f0efe33bd2 Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Wed, 10 Feb 2021 17:19:34 +0200 Subject: [PATCH 05/32] [Telemetry] use fresh keys (#90446) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/encryption/encrypt.test.ts | 14 +++++++------- .../server/encryption/encrypt.ts | 2 +- .../server/encryption/telemetry_jwks.ts | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts index 9d4443d6e703..c1a1a32e3c7f 100644 --- a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts +++ b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts @@ -14,13 +14,13 @@ describe('getKID', () => { it(`returns 'kibana_dev' kid for development`, async () => { const useProdKey = false; const kid = getKID(useProdKey); - expect(kid).toBe('kibana_dev'); + expect(kid).toBe('kibana_dev1'); }); - it(`returns 'kibana_prod' kid for development`, async () => { + it(`returns 'kibana_1' kid for production`, async () => { const useProdKey = true; const kid = getKID(useProdKey); - expect(kid).toBe('kibana'); + expect(kid).toBe('kibana1'); }); }); @@ -35,15 +35,15 @@ describe('encryptTelemetry', () => { expect(createRequestEncryptor).toBeCalledWith(telemetryJWKS); }); - it('uses kibana kid on { useProdKey: true }', async () => { + it('uses kibana1 kid on { useProdKey: true }', async () => { const payload = { some: 'value' }; await encryptTelemetry(payload, { useProdKey: true }); - expect(mockEncrypt).toBeCalledWith('kibana', payload); + expect(mockEncrypt).toBeCalledWith('kibana1', payload); }); - it('uses kibana_dev kid on { useProdKey: false }', async () => { + it('uses kibana_dev1 kid on { useProdKey: false }', async () => { const payload = { some: 'value' }; await encryptTelemetry(payload, { useProdKey: false }); - expect(mockEncrypt).toBeCalledWith('kibana_dev', payload); + expect(mockEncrypt).toBeCalledWith('kibana_dev1', payload); }); }); diff --git a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts index 331aedce8cbc..a2c24627f6fd 100644 --- a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts +++ b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts @@ -10,7 +10,7 @@ import { createRequestEncryptor } from '@elastic/request-crypto'; import { telemetryJWKS } from './telemetry_jwks'; export function getKID(useProdKey = false): string { - return useProdKey ? 'kibana' : 'kibana_dev'; + return useProdKey ? 'kibana1' : 'kibana_dev1'; } export async function encryptTelemetry( diff --git a/src/plugins/telemetry_collection_manager/server/encryption/telemetry_jwks.ts b/src/plugins/telemetry_collection_manager/server/encryption/telemetry_jwks.ts index c3ac71969e44..bf4c2a952c43 100644 --- a/src/plugins/telemetry_collection_manager/server/encryption/telemetry_jwks.ts +++ b/src/plugins/telemetry_collection_manager/server/encryption/telemetry_jwks.ts @@ -12,21 +12,21 @@ export const telemetryJWKS: PublicJWKS = { keys: [ { kty: 'RSA', - kid: 'kibana', + kid: 'kibana1', use: 'enc', alg: 'RSA-OAEP', e: 'AQAB', n: - 'xYYa5XzvENaAzElCxQurloQM2KEQ058YSjZqmOwa-IN-EZMSUaYPY3qfYCG78ioRaKTHq4mgnkyrDKgjY_1pWKytiRD61FG2ZUeOCwzydnqO8Qpz2vFnibEHkZBRsKkLHgm90RgGpcXfz8vwxkz_nu59aWy5Qr7Ct99H0pEV1HoiCvy5Yw3QfWSAeV-3DWmq_0kX49tqk5yZE-vKnUhNMgqM22lMFTE5-vlaeHgv4ZcvCQx_HrOeea8LyZa5YOdqN-9st0g0G-aWp3CNI2-KJlMUTBAfIAtjwmJ-8QlgeIB1aA7OI2Ceh3kd4dNLesGdLvZ0y4f8IMOsO1dsRWSEsQ', + 'gjwVNVkOqbTZ6QrxdeYbKDnBzhCZGXM97Iq0dXJlpa-7UegcBemI1ALZkbX6AaDrCmqzetsnxJbVz2gr-uzkc97zzjTvPAn-jM-9cfjfsb-nd70qLY7ru3qdyOLb5-ho8cjmjnAp7VaEPuiNOjZ6V6tXq8Cn5LHH8G6K8oZLU1N4RWqkcAvEIlfaLusfMnl15fe7aZkYaKfVFjD-pti_2JGRV9XZ0knRI2oIRMaroBYpfYJxbpR0NLhR7ND6U5WlvxfnaVvRK4c_plVLOtcROqZVn98Z8yZ6GU14vCcvkIBox2D_xd1gSkpMammTQ3tVAEAvoq_wEn_qEbls1Uucgw', }, { kty: 'RSA', - kid: 'kibana_dev', + kid: 'kibana_dev1', use: 'enc', alg: 'RSA-OAEP', e: 'AQAB', n: - 'juVHivsYFznjrDC449oL3xKVTvux_7dEgBGOgJdfzA2R2GspEAOzupT-VkBnqrJnRP_lznM8bQIvvst1f_DNQ1me_Lr9u9cwL5Vq6SWlmw_u9ur_-ewkShU4tBoJDArksOS-ciTaUJoMaxanb7jWexp0pCDlrLrQyAOCnKQL701mD1gdT4rIw7F-jkb5fLUNUVzOGaGyVy6DHAHZx7Tnyw8rswhyRVvuS73imbRp9XcdOFhBDOeSbrSuZGqrVCjoIlWw-UsiW2ueRd8brBoOIHSmTOMIrIMjpPmzMFRKyCvvhnbjrw8j3fQtFII8urhXCVAw8aIHZhiBc5t9ZuwbJw', + 'rEi54h-9hCbqy9Mj_tJmx-dJdtrMmMzkhX5Wd63Pp3dABHpnLJSy--y8QoEa9K9ACaRfExSxgYQ-3K17Yy-UYj3ChAl3hrqZcP2AT3O18Lr2BN7EBjy88lTM0oeck9KLL_iGf8wz8_jeqQFIo3AWrBBuR3VFE0_k-_N1KCenSVm_fE3Nk_ZXm1ByFbgxWUFrYgLfEQn2v0FQYVpfTlbV_awtqoZLYGtuHmaLZhErzJFh6W8zrx8oSpGn8VlVLjF-AR3ugfw2F_HM8ZR8zY1dHVxvoLGz13F5aY8DHn0_ao9t0Yz2Y_SUNviyxMx0eIEJeo2njM2vMzYQNaT1Ghgc-w', }, ], }; From a60e4752609ccff554c3e76ca1a853afe83390f5 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Wed, 10 Feb 2021 15:30:43 +0000 Subject: [PATCH 06/32] [Security Solution] Split test cases into blocks (#89978) * split test cases into blocks * run tests individually * fix up timeline creation * fix up timeline creation * add assertions for timeline creation * create timeline by api * create timeline by api * fix lint error * add selector to timeline screen * fix type error Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../integration/timelines/creation.spec.ts | 97 ++++++++--------- .../integration/timelines/notes_tab.spec.ts | 60 ++++++++++ .../timelines/open_timeline.spec.ts | 103 ++++++++++++++++++ .../integration/timelines/query_tab.spec.ts | 77 +++++++++++++ .../cypress/screens/timeline.ts | 4 + .../cypress/tasks/api_calls/notes.ts | 23 ++++ .../cypress/tasks/timeline.ts | 27 ++++- 7 files changed, 336 insertions(+), 55 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/timelines/open_timeline.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/tasks/api_calls/notes.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts index 3c1a49bbf179..b08bae26bf7e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts @@ -10,25 +10,12 @@ import { timeline } from '../../objects/timeline'; import { FAVORITE_TIMELINE, LOCKED_ICON, - UNLOCKED_ICON, - NOTES_TAB_BUTTON, NOTES_TEXT, - // NOTES_COUNT, - NOTES_TEXT_AREA, PIN_EVENT, - TIMELINE_DESCRIPTION, TIMELINE_FILTER, - // TIMELINE_FILTER, - TIMELINE_QUERY, - TIMELINE_TITLE, - OPEN_TIMELINE_MODAL, + TIMELINE_PANEL, } from '../../screens/timeline'; -import { - TIMELINES_DESCRIPTION, - TIMELINES_PINNED_EVENT_COUNT, - TIMELINES_NOTES_COUNT, - TIMELINES_FAVORITE, -} from '../../screens/timelines'; + import { cleanKibana } from '../../tasks/common'; import { loginAndWaitForPage } from '../../tasks/login'; @@ -39,70 +26,72 @@ import { addNotesToTimeline, closeTimeline, createNewTimeline, + goToQueryTab, markAsFavorite, - openTimelineFromSettings, pinFirstEvent, populateTimeline, waitForTimelineChanges, } from '../../tasks/timeline'; -import { openTimeline } from '../../tasks/timelines'; import { OVERVIEW_URL } from '../../urls/navigation'; -// Skipped at the moment as there looks to be in-deterministic bugs with the open timeline dialog. -describe.skip('Timelines', () => { - beforeEach(() => { +describe('Timelines', (): void => { + before(() => { cleanKibana(); + loginAndWaitForPage(OVERVIEW_URL); }); - it('Creates a timeline', () => { - cy.intercept('PATCH', '/api/timeline').as('timeline'); + describe('Toggle create timeline from plus icon', () => { + after(() => { + closeTimeline(); + }); - loginAndWaitForPage(OVERVIEW_URL); - openTimelineUsingToggle(); - addNameAndDescriptionToTimeline(timeline); + it('toggle create timeline ', () => { + createNewTimeline(); + cy.get(TIMELINE_PANEL).should('be.visible'); + }); + }); - cy.wait('@timeline').then(({ response }) => { - const timelineId = response!.body.data.persistTimeline.timeline.savedObjectId; + describe('Creates a timeline by clicking untitled timeline from bottom bar', () => { + after(() => { + closeTimeline(); + }); + before(() => { + openTimelineUsingToggle(); + addNameAndDescriptionToTimeline(timeline); populateTimeline(); + }); + + beforeEach(() => { + goToQueryTab(); + }); + + it('can be added filter', () => { addFilter(timeline.filter); - pinFirstEvent(); + cy.get(TIMELINE_FILTER(timeline.filter)).should('exist'); + }); + it('pins an event', () => { + pinFirstEvent(); cy.get(PIN_EVENT) .should('have.attr', 'aria-label') .and('match', /Unpin the event in row 2/); + }); + + it('has a lock icon', () => { cy.get(LOCKED_ICON).should('be.visible'); + }); + it('can be added notes', () => { addNotesToTimeline(timeline.notes); + cy.get(NOTES_TEXT).should('have.text', timeline.notes); + }); + + it('can be marked as favorite', () => { markAsFavorite(); waitForTimelineChanges(); - createNewTimeline(); - closeTimeline(); - openTimelineFromSettings(); - - cy.get(OPEN_TIMELINE_MODAL).should('be.visible'); - cy.contains(timeline.title).should('exist'); - cy.get(TIMELINES_DESCRIPTION).first().should('have.text', timeline.description); - cy.get(TIMELINES_PINNED_EVENT_COUNT).first().should('have.text', '1'); - cy.get(TIMELINES_NOTES_COUNT).first().should('have.text', '1'); - cy.get(TIMELINES_FAVORITE).first().should('exist'); - - openTimeline(timelineId); - - cy.get(FAVORITE_TIMELINE).should('exist'); - cy.get(TIMELINE_TITLE).should('have.text', timeline.title); - cy.get(TIMELINE_DESCRIPTION).should('have.text', timeline.description); // This is the flake part where it sometimes does not show/load the timelines correctly - cy.get(TIMELINE_QUERY).should('have.text', `${timeline.query} `); - cy.get(TIMELINE_FILTER(timeline.filter)).should('exist'); - cy.get(PIN_EVENT) - .should('have.attr', 'aria-label') - .and('match', /Unpin the event in row 2/); - cy.get(UNLOCKED_ICON).should('be.visible'); - cy.get(NOTES_TAB_BUTTON).click(); - cy.get(NOTES_TEXT_AREA).should('exist'); - - cy.get(NOTES_TEXT).should('have.text', timeline.notes); + cy.get(FAVORITE_TIMELINE).should('have.text', 'Remove from favorites'); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts new file mode 100644 index 000000000000..6653290fc2eb --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts @@ -0,0 +1,60 @@ +/* + * 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 { timeline } from '../../objects/timeline'; + +import { NOTES_TEXT, NOTES_TEXT_AREA } from '../../screens/timeline'; +import { createTimeline } from '../../tasks/api_calls/timelines'; + +import { cleanKibana } from '../../tasks/common'; + +import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; +import { + addNotesToTimeline, + closeTimeline, + goToNotesTab, + openTimelineById, + waitForEventsPanelToBeLoaded, +} from '../../tasks/timeline'; +import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; + +import { TIMELINES_URL } from '../../urls/navigation'; + +describe('Timeline notes tab', () => { + let timelineId: string | null = null; + before(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); + waitForTimelinesPanelToBeLoaded(); + + createTimeline(timeline) + .then((response) => { + timelineId = response.body.data.persistTimeline.timeline.savedObjectId; + }) + .then(() => { + waitForTimelinesPanelToBeLoaded(); + openTimelineById(timelineId!) + .click({ force: true }) + .then(() => { + waitForEventsPanelToBeLoaded(); + goToNotesTab(); + addNotesToTimeline(timeline.notes); + }); + }); + }); + after(() => { + closeTimeline(); + }); + + it('should contain notes', () => { + cy.get(NOTES_TEXT).should('have.text', timeline.notes); + }); + + it('should render mockdown', () => { + cy.get(NOTES_TEXT_AREA).should('exist'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/open_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/open_timeline.spec.ts new file mode 100644 index 000000000000..5d5d125082b8 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/open_timeline.spec.ts @@ -0,0 +1,103 @@ +/* + * 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 { timeline } from '../../objects/timeline'; + +import { TIMELINE_DESCRIPTION, TIMELINE_TITLE, OPEN_TIMELINE_MODAL } from '../../screens/timeline'; +import { + TIMELINES_DESCRIPTION, + TIMELINES_PINNED_EVENT_COUNT, + TIMELINES_NOTES_COUNT, + TIMELINES_FAVORITE, +} from '../../screens/timelines'; +import { addNoteToTimeline } from '../../tasks/api_calls/notes'; + +import { createTimeline } from '../../tasks/api_calls/timelines'; + +import { cleanKibana } from '../../tasks/common'; + +import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; +import { + closeOpenTimelineModal, + markAsFavorite, + openTimelineById, + openTimelineFromSettings, + pinFirstEvent, + waitForEventsPanelToBeLoaded, +} from '../../tasks/timeline'; +import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; + +import { TIMELINES_URL } from '../../urls/navigation'; + +describe('Open timeline', () => { + let timelineId: string | null = null; + before(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); + waitForTimelinesPanelToBeLoaded(); + + createTimeline(timeline) + .then((response) => { + timelineId = response.body.data.persistTimeline.timeline.savedObjectId; + }) + .then(() => { + const note = timeline.notes; + addNoteToTimeline(note, timelineId!).should((response) => { + expect(response.status).to.equal(200); + waitForTimelinesPanelToBeLoaded(); + openTimelineById(timelineId!) + .click({ force: true }) + .then(() => { + waitForEventsPanelToBeLoaded(); + pinFirstEvent(); + markAsFavorite(); + }); + }); + }); + }); + describe('Open timeline modal', () => { + before(() => { + openTimelineFromSettings(); + }); + + after(() => { + closeOpenTimelineModal(); + }); + + it('should open a modal', () => { + cy.get(OPEN_TIMELINE_MODAL).should('be.visible'); + }); + + it('should display timeline info - title', () => { + cy.contains(timeline.title).should('exist'); + }); + + it('should display timeline info - description', () => { + cy.get(TIMELINES_DESCRIPTION).first().should('have.text', timeline.description); + }); + + it('should display timeline info - pinned event count', () => { + cy.get(TIMELINES_PINNED_EVENT_COUNT).first().should('have.text', '1'); + }); + + it('should display timeline info - notes count', () => { + cy.get(TIMELINES_NOTES_COUNT).first().should('have.text', '1'); + }); + + it('should display timeline info - favorite timeline', () => { + cy.get(TIMELINES_FAVORITE).first().should('exist'); + }); + + it('should display timeline content - title', () => { + cy.get(TIMELINE_TITLE).should('have.text', timeline.title); + }); + + it('should display timeline content - description', () => { + cy.get(TIMELINE_DESCRIPTION).should('have.text', timeline.description); // This is the flake part where it sometimes does not show/load the timelines correctly + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts new file mode 100644 index 000000000000..56cb5d870d79 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/query_tab.spec.ts @@ -0,0 +1,77 @@ +/* + * 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 { timeline } from '../../objects/timeline'; + +import { UNLOCKED_ICON, PIN_EVENT, TIMELINE_FILTER, TIMELINE_QUERY } from '../../screens/timeline'; +import { addNoteToTimeline } from '../../tasks/api_calls/notes'; +import { createTimeline } from '../../tasks/api_calls/timelines'; + +import { cleanKibana } from '../../tasks/common'; + +import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; +import { + addFilter, + closeTimeline, + openTimelineById, + pinFirstEvent, + waitForEventsPanelToBeLoaded, +} from '../../tasks/timeline'; +import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; + +import { TIMELINES_URL } from '../../urls/navigation'; + +describe('Timeline query tab', () => { + let timelineId: string | null = null; + before(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); + waitForTimelinesPanelToBeLoaded(); + + createTimeline(timeline) + .then((response) => { + timelineId = response.body.data.persistTimeline.timeline.savedObjectId; + }) + .then(() => { + const note = timeline.notes; + addNoteToTimeline(note, timelineId!).should((response) => { + expect(response.status).to.equal(200); + waitForTimelinesPanelToBeLoaded(); + openTimelineById(timelineId!) + .click({ force: true }) + .then(() => { + waitForEventsPanelToBeLoaded(); + pinFirstEvent(); + addFilter(timeline.filter); + }); + }); + }); + }); + + describe('Query tab', () => { + after(() => { + closeTimeline(); + }); + it('should contain the right query', () => { + cy.get(TIMELINE_QUERY).should('have.text', `${timeline.query}`); + }); + + it('should display timeline filter', () => { + cy.get(TIMELINE_FILTER(timeline.filter)).should('exist'); + }); + + it('should display pinned events', () => { + cy.get(PIN_EVENT) + .should('have.attr', 'aria-label') + .and('match', /Unpin the event in row 2/); + }); + + it('should have an unlock icon', () => { + cy.get(UNLOCKED_ICON).should('be.visible'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 78bc091e8db7..92f96a591ab5 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -73,6 +73,8 @@ export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]'; export const OPEN_TIMELINE_MODAL = '[data-test-subj="open-timeline-modal"]'; +export const CLOSE_OPEN_TIMELINE_MODAL_BTN = `${OPEN_TIMELINE_MODAL} > button`; + export const OPEN_TIMELINE_TEMPLATE_ICON = '[data-test-subj="open-timeline-modal-body-filter-template"]'; @@ -148,6 +150,8 @@ export const TIMELINE_FLYOUT_BODY = '[data-test-subj="query-tab-flyout-body"]'; export const TIMELINE_INSPECT_BUTTON = `${TIMELINE_FLYOUT} [data-test-subj="inspect-icon-button"]`; +export const TIMELINE_PANEL = `[data-test-subj="timeline-flyout-header-panel"]`; + export const TIMELINE_QUERY = '[data-test-subj="timelineQueryInput"]'; export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-plus-in-circle"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/notes.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/notes.ts new file mode 100644 index 000000000000..0fc1a8639560 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/notes.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const addNoteToTimeline = (note: string, timelineId: string) => + cy.request({ + method: 'POST', + url: '/api/solutions/security/graphql', + body: { + operationName: 'PersistTimelineNoteMutation', + variables: { + noteId: null, + version: null, + note: { note, timelineId }, + }, + query: + 'mutation PersistTimelineNoteMutation($noteId: ID, $version: String, $note: NoteInput!) {\n persistNote(noteId: $noteId, version: $version, note: $note) {\n code\n message\n note {\n eventId\n note\n timelineId\n timelineVersion\n noteId\n created\n createdBy\n updated\n updatedBy\n version\n __typename\n }\n __typename\n }\n}\n', + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index edaa5be487a0..ca4c869e0f2d 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -50,6 +50,7 @@ import { TIMELINE_EDIT_MODAL_OPEN_BUTTON, TIMELINE_EDIT_MODAL_SAVE_BUTTON, QUERY_TAB_BUTTON, + CLOSE_OPEN_TIMELINE_MODAL_BTN, } from '../screens/timeline'; import { TIMELINES_TABLE } from '../screens/timelines'; @@ -83,8 +84,20 @@ export const addNameAndDescriptionToTimeline = (timeline: Timeline) => { cy.get(TIMELINE_TITLE_INPUT).should('not.exist'); }; +export const goToNotesTab = () => { + return cy.get(NOTES_TAB_BUTTON).click({ force: true }); +}; + +export const getNotePreviewByNoteId = (noteId: string) => { + return cy.get(`[data-test-subj="note-preview-${noteId}"]`); +}; + +export const goToQueryTab = () => { + cy.get(QUERY_TAB_BUTTON).click({ force: true }); +}; + export const addNotesToTimeline = (notes: string) => { - cy.get(NOTES_TAB_BUTTON).click(); + goToNotesTab(); cy.get(NOTES_TEXT_AREA).type(notes); cy.get(ADD_NOTE_BUTTON).click(); cy.get(QUERY_TAB_BUTTON).click(); @@ -123,6 +136,10 @@ export const checkIdToggleField = () => { }); }; +export const closeOpenTimelineModal = () => { + cy.get(CLOSE_OPEN_TIMELINE_MODAL_BTN).click({ force: true }); +}; + export const closeTimeline = () => { cy.get(CLOSE_TIMELINE_BTN).filter(':visible').click({ force: true }); }; @@ -170,6 +187,10 @@ export const openTimelineTemplateFromSettings = (id: string) => { cy.get(TIMELINE_TITLE_BY_ID(id)).click({ force: true }); }; +export const openTimelineById = (timelineId: string) => { + return cy.get(TIMELINE_TITLE_BY_ID(timelineId)).click({ force: true }); +}; + export const pinFirstEvent = () => { cy.get(PIN_EVENT).first().click({ force: true }); }; @@ -223,3 +244,7 @@ export const waitForTimelineChanges = () => { export const waitForTimelinesPanelToBeLoaded = () => { cy.get(TIMELINES_TABLE).should('exist'); }; + +export const waitForEventsPanelToBeLoaded = () => { + cy.get(QUERY_TAB_BUTTON).find('.euiBadge').should('exist'); +}; From bcdc022efedcf949abfcef58f5705caba3c28bac Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Wed, 10 Feb 2021 08:40:13 -0700 Subject: [PATCH 07/32] [Maps] Add redux devtools back to Maps (#90863) --- .../plugins/maps/public/reducers/non_serializable_instances.js | 1 - x-pack/plugins/maps/public/reducers/store.js | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/maps/public/reducers/non_serializable_instances.js b/x-pack/plugins/maps/public/reducers/non_serializable_instances.js index 4cc4e91a308a..402d7727cd6f 100644 --- a/x-pack/plugins/maps/public/reducers/non_serializable_instances.js +++ b/x-pack/plugins/maps/public/reducers/non_serializable_instances.js @@ -77,7 +77,6 @@ export const getEventHandlers = ({ nonSerializableInstances }) => { }; export function getChartsPaletteServiceGetColor({ nonSerializableInstances }) { - console.log('getChartsPaletteServiceGetColor', nonSerializableInstances); return nonSerializableInstances.chartsPaletteServiceGetColor; } diff --git a/x-pack/plugins/maps/public/reducers/store.js b/x-pack/plugins/maps/public/reducers/store.js index 4e355add59fe..76199de5b24c 100644 --- a/x-pack/plugins/maps/public/reducers/store.js +++ b/x-pack/plugins/maps/public/reducers/store.js @@ -36,5 +36,6 @@ export function createMapStore() { }; const storeConfig = {}; - return createStore(rootReducer, storeConfig, compose(...enhancers)); + const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + return createStore(rootReducer, storeConfig, composeEnhancers(...enhancers)); } From e94a164b7eb11939eb31d1a53e29dfcec93b09c0 Mon Sep 17 00:00:00 2001 From: Bohdan Tsymbala Date: Wed, 10 Feb 2021 16:47:56 +0100 Subject: [PATCH 08/32] Initial version of adding artifacts per policy to the manifest. (#89130) * Initial version of adding artifacts per policy to the manifest. * Minor renaming to convey the purpose of the variable. * Added ability to override list item mock data. * Changed function signature to be more reusable. * Implementationg of support of artifacts per policy in the manifest data structure. * Added saved objects migrations. * Renamed the endpoint to reflect that it's artifacts endpoint. * Fixed tests. * Fixed the manifest data. * Fixed linting errors (result of merge). * Updated ES mappings for manifest in all test setups. * Updated hash in the mappings. * Fixed the typo that lead to failing test. * Fixed the problem with manifest not being dispatched to policies if there are same artifact names but different content. Artifact name in the ManifestSchema is not unique id, hence added decoded_sha256 to the comparison. Added test case to cover this. * Fixed the problem with the task flow when failure to dispatch to policies will result in commited manifest and no redispatch on next task run. Changed tests to reflect new flow (actually restored previous flow). * Forgot to commit changes in mock. * Made other tests more readable using same varialbe naming pattern. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../es_archiver/apm_8.0.0/mappings.json | 21 +- .../exception_list_item_schema.mock.ts | 5 +- .../endpoint/ingest_integration.test.ts | 304 +++-- .../server/endpoint/ingest_integration.ts | 36 +- .../server/endpoint/lib/artifacts/lists.ts | 18 +- .../endpoint/lib/artifacts/manifest.test.ts | 828 ++++++++++--- .../server/endpoint/lib/artifacts/manifest.ts | 169 +-- .../endpoint/lib/artifacts/migrations.test.ts | 57 + .../endpoint/lib/artifacts/migrations.ts | 35 + .../server/endpoint/lib/artifacts/mocks.ts | 146 ++- .../lib/artifacts/saved_object_mappings.ts | 17 +- .../endpoint/lib/artifacts/task.test.ts | 277 ++++- .../server/endpoint/lib/artifacts/task.ts | 42 +- ...list.test.ts => download_artifact.test.ts} | 4 +- ...exception_list.ts => download_artifact.ts} | 6 +- .../server/endpoint/routes/artifacts/index.ts | 2 +- .../schemas/artifacts/saved_objects.mock.ts | 12 +- .../schemas/artifacts/saved_objects.ts | 10 +- .../artifacts/artifact_client.mock.ts | 19 - .../artifacts/artifact_client.test.ts | 7 +- .../manifest_manager/manifest_manager.mock.ts | 118 +- .../manifest_manager/manifest_manager.test.ts | 1078 ++++++++++++----- .../manifest_manager/manifest_manager.ts | 173 +-- .../security_solution/server/plugin.ts | 10 +- .../es_archiver/apm_8.0.0/mappings.json | 21 +- .../es_archiver/key_rotation/mappings.json | 19 +- .../es_archives/actions/mappings.json | 21 +- .../es_archives/alerts_legacy/mappings.json | 21 +- .../es_archives/canvas/filter/mappings.json | 21 +- .../es_archives/canvas/reports/mappings.json | 21 +- .../es_archives/cases/mappings.json | 21 +- .../data/search_sessions/mappings.json | 19 +- .../endpoint/artifacts/api_feature/data.json | 12 +- .../telemetry/agent_only/mappings.json | 21 +- .../mappings.json | 21 +- .../cloned_endpoint_installed/mappings.json | 21 +- .../cloned_endpoint_uninstalled/mappings.json | 21 +- .../endpoint_malware_disabled/mappings.json | 21 +- .../endpoint_malware_enabled/mappings.json | 21 +- .../endpoint_uninstalled/mappings.json | 21 +- .../event_log_multiple_indicies/mappings.json | 4 +- .../es_archives/lists/mappings.json | 24 +- .../canvas_disallowed_url/mappings.json | 20 +- .../reporting/ecommerce_kibana/mappings.json | 21 +- .../ecommerce_kibana_spaces/mappings.json | 21 +- .../task_manager_removed_types/mappings.json | 4 +- .../visualize/default/mappings.json | 21 +- 47 files changed, 2717 insertions(+), 1115 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrations.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrations.ts rename x-pack/plugins/security_solution/server/endpoint/routes/artifacts/{download_exception_list.test.ts => download_artifact.test.ts} (98%) rename x-pack/plugins/security_solution/server/endpoint/routes/artifacts/{download_exception_list.ts => download_artifact.ts} (94%) delete mode 100644 x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.mock.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/es_archiver/apm_8.0.0/mappings.json b/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/es_archiver/apm_8.0.0/mappings.json index 13bfec74269b..9f84f4885c4c 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/es_archiver/apm_8.0.0/mappings.json +++ b/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/es_archiver/apm_8.0.0/mappings.json @@ -31,7 +31,7 @@ "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", "dashboard": "40554caf09725935e2c02e02563a2d07", "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", - "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862", "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", "epm-packages": "2b83397e3eaaaa8ef15e38813f3721c3", "exception-list": "67f055ab8c10abd7b2ebfd969b836788", @@ -818,16 +818,25 @@ "index": false, "type": "date" }, - "ids": { - "index": false, - "type": "keyword" - }, "schemaVersion": { "type": "keyword" }, "semanticVersion": { "index": false, "type": "keyword" + }, + "artifacts": { + "type": "nested", + "properties": { + "policyId": { + "type": "keyword", + "index": false + }, + "artifactId": { + "type": "keyword", + "index": false + } + } } } }, @@ -22352,4 +22361,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts index e2c5ea504bd4..ab2aac39c19d 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts @@ -24,7 +24,9 @@ import { import { ExceptionListItemSchema } from './exception_list_item_schema'; -export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({ +export const getExceptionListItemSchemaMock = ( + overrides?: Partial +): ExceptionListItemSchema => ({ _version: undefined, comments: COMMENTS, created_at: DATE_NOW, @@ -43,6 +45,7 @@ export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({ type: ITEM_TYPE, updated_at: DATE_NOW, updated_by: USER, + ...(overrides || {}), }); export const getExceptionListItemSchemaXMock = (count = 1): ExceptionListItemSchema[] => { diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts index f4cdb28a01b8..733e25947347 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts @@ -11,10 +11,7 @@ import { policyFactory, policyFactoryWithoutPaidFeatures, } from '../../common/endpoint/models/policy_config'; -import { - getManifestManagerMock, - ManifestManagerMockType, -} from './services/artifacts/manifest_manager/manifest_manager.mock'; +import { buildManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock'; import { getPackagePolicyCreateCallback, getPackagePolicyUpdateCallback, @@ -32,6 +29,12 @@ import { ProtectionModes } from '../../common/endpoint/types'; import type { SecuritySolutionRequestHandlerContext } from '../types'; import { getExceptionListClientMock } from '../../../lists/server/services/exception_lists/exception_list_client.mock'; import { ExceptionListClient } from '../../../lists/server'; +import { InternalArtifactCompleteSchema } from './schemas/artifacts'; +import { ManifestManager } from './services/artifacts/manifest_manager'; +import { getMockArtifacts, toArtifactRecords } from './lib/artifacts/mocks'; +import { Manifest } from './lib/artifacts'; +import { NewPackagePolicy } from '../../../fleet/common/types/models'; +import { ManifestSchema } from '../../common/endpoint/schema/manifest'; describe('ingest_integration tests ', () => { let endpointAppContextMock: EndpointAppContextServiceStartContract; @@ -53,21 +56,25 @@ describe('ingest_integration tests ', () => { licenseService = new LicenseService(); licenseService.start(licenseEmitter); }); + afterEach(() => { licenseService.stop(); licenseEmitter.complete(); }); - describe('ingest_integration sanity checks', () => { - beforeEach(() => { - licenseEmitter.next(Platinum); // set license level to platinum + describe('package policy init callback (atifacts manifest initialisation tests)', () => { + const createNewEndpointPolicyInput = (manifest: ManifestSchema) => ({ + type: 'endpoint', + enabled: true, + streams: [], + config: { + policy: { value: policyFactory() }, + artifact_manifest: { value: manifest }, + }, }); - test('policy is updated with initial manifest', async () => { - const logger = loggingSystemMock.create().get('ingest_integration.test'); - const manifestManager = getManifestManagerMock({ - mockType: ManifestManagerMockType.InitialSystemState, - }); + const invokeCallback = async (manifestManager: ManifestManager): Promise => { + const logger = loggingSystemMock.create().get('ingest_integration.test'); const callback = getPackagePolicyCreateCallback( logger, manifestManager, @@ -78,175 +85,153 @@ describe('ingest_integration tests ', () => { licenseService, exceptionListClient ); - const policyConfig = createNewPackagePolicyMock(); // policy config without manifest - const newPolicyConfig = await callback(policyConfig, ctx, req); // policy config WITH manifest - expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); - expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory()); - expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual({ - artifacts: { - 'endpoint-exceptionlist-macos-v1': { - compression_algorithm: 'zlib', - decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-exceptionlist-windows-v1': { - compression_algorithm: 'zlib', - decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - decoded_size: 14, - encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - encoded_size: 22, - encryption_algorithm: 'none', - relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-trustlist-linux-v1': { - compression_algorithm: 'zlib', - decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - decoded_size: 287, - encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', - encoded_size: 133, - encryption_algorithm: 'none', - relative_url: - '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - }, - 'endpoint-trustlist-macos-v1': { - compression_algorithm: 'zlib', - decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - decoded_size: 287, - encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', - encoded_size: 133, - encryption_algorithm: 'none', - relative_url: - '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - }, - 'endpoint-trustlist-windows-v1': { - compression_algorithm: 'zlib', - decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - decoded_size: 287, - encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', - encoded_size: 133, - encryption_algorithm: 'none', - relative_url: - '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - }, - }, - manifest_version: '1.0.0', - schema_version: 'v1', - }); + return callback(createNewPackagePolicyMock(), ctx, req); + }; + + const TEST_POLICY_ID_1 = 'c6d16e42-c32d-4dce-8a88-113cfe276ad1'; + const TEST_POLICY_ID_2 = '93c46720-c217-11ea-9906-b5b8a21b268e'; + const ARTIFACT_NAME_EXCEPTIONS_MACOS = 'endpoint-exceptionlist-macos-v1'; + const ARTIFACT_NAME_TRUSTED_APPS_MACOS = 'endpoint-trustlist-macos-v1'; + const ARTIFACT_NAME_TRUSTED_APPS_WINDOWS = 'endpoint-trustlist-windows-v1'; + let ARTIFACT_EXCEPTIONS_MACOS: InternalArtifactCompleteSchema; + let ARTIFACT_EXCEPTIONS_WINDOWS: InternalArtifactCompleteSchema; + let ARTIFACT_TRUSTED_APPS_MACOS: InternalArtifactCompleteSchema; + let ARTIFACT_TRUSTED_APPS_WINDOWS: InternalArtifactCompleteSchema; + + beforeAll(async () => { + const artifacts = await getMockArtifacts({ compress: true }); + ARTIFACT_EXCEPTIONS_MACOS = artifacts[0]; + ARTIFACT_EXCEPTIONS_WINDOWS = artifacts[1]; + ARTIFACT_TRUSTED_APPS_MACOS = artifacts[2]; + ARTIFACT_TRUSTED_APPS_WINDOWS = artifacts[3]; }); - test('policy is returned even if error is encountered during artifact creation', async () => { - const logger = loggingSystemMock.create().get('ingest_integration.test'); - const manifestManager = getManifestManagerMock(); - manifestManager.pushArtifacts = jest.fn().mockResolvedValue([new Error('error updating')]); - const lastComputed = await manifestManager.getLastComputedManifest(); + beforeEach(() => { + licenseEmitter.next(Platinum); // set license level to platinum + }); - const callback = getPackagePolicyCreateCallback( - logger, - manifestManager, - endpointAppContextMock.appClientFactory, - maxTimelineImportExportSize, - endpointAppContextMock.security, - endpointAppContextMock.alerts, - licenseService, - exceptionListClient - ); - const policyConfig = createNewPackagePolicyMock(); - const newPolicyConfig = await callback(policyConfig, ctx, req); + test('default manifest is taken when there is none and there are errors building new one', async () => { + const manifestManager = buildManifestManagerMock(); + manifestManager.getLastComputedManifest = jest.fn().mockResolvedValue(null); + manifestManager.buildNewManifest = jest.fn().mockRejectedValue(new Error()); - expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); - expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory()); - expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( - lastComputed!.toEndpointFormat() + expect((await invokeCallback(manifestManager)).inputs[0]).toStrictEqual( + createNewEndpointPolicyInput({ + artifacts: {}, + manifest_version: '1.0.0', + schema_version: 'v1', + }) ); + + expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(); + expect(manifestManager.pushArtifacts).not.toHaveBeenCalled(); + expect(manifestManager.commit).not.toHaveBeenCalled(); }); - test('initial policy creation succeeds if manifest retrieval fails', async () => { - const logger = loggingSystemMock.create().get('ingest_integration.test'); - const manifestManager = getManifestManagerMock({ - mockType: ManifestManagerMockType.InitialSystemState, - }); - const lastComputed = await manifestManager.getLastComputedManifest(); - expect(lastComputed).toEqual(null); + test('default manifest is taken when there is none and there are errors pushing artifacts', async () => { + const newManifest = Manifest.getDefault(); + newManifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); - manifestManager.buildNewManifest = jest.fn().mockRejectedValue(new Error('abcd')); - const callback = getPackagePolicyCreateCallback( - logger, - manifestManager, - endpointAppContextMock.appClientFactory, - maxTimelineImportExportSize, - endpointAppContextMock.security, - endpointAppContextMock.alerts, - licenseService, - exceptionListClient + const manifestManager = buildManifestManagerMock(); + manifestManager.getLastComputedManifest = jest.fn().mockResolvedValue(null); + manifestManager.buildNewManifest = jest.fn().mockResolvedValue(newManifest); + manifestManager.pushArtifacts = jest.fn().mockResolvedValue([new Error()]); + + expect((await invokeCallback(manifestManager)).inputs[0]).toStrictEqual( + createNewEndpointPolicyInput({ + artifacts: {}, + manifest_version: '1.0.0', + schema_version: 'v1', + }) ); - const policyConfig = createNewPackagePolicyMock(); - const newPolicyConfig = await callback(policyConfig, ctx, req); - expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); - expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory()); + expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([ARTIFACT_EXCEPTIONS_MACOS]); + expect(manifestManager.commit).not.toHaveBeenCalled(); }); - test('subsequent policy creations succeed', async () => { - const logger = loggingSystemMock.create().get('ingest_integration.test'); - const manifestManager = getManifestManagerMock(); - const lastComputed = await manifestManager.getLastComputedManifest(); + test('default manifest is taken when there is none and there are errors commiting manifest', async () => { + const newManifest = Manifest.getDefault(); + newManifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); - manifestManager.buildNewManifest = jest.fn().mockResolvedValue(lastComputed); // no diffs - const callback = getPackagePolicyCreateCallback( - logger, - manifestManager, - endpointAppContextMock.appClientFactory, - maxTimelineImportExportSize, - endpointAppContextMock.security, - endpointAppContextMock.alerts, - licenseService, - exceptionListClient - ); - const policyConfig = createNewPackagePolicyMock(); - const newPolicyConfig = await callback(policyConfig, ctx, req); + const manifestManager = buildManifestManagerMock(); + manifestManager.getLastComputedManifest = jest.fn().mockResolvedValue(null); + manifestManager.buildNewManifest = jest.fn().mockResolvedValue(newManifest); + manifestManager.pushArtifacts = jest.fn().mockResolvedValue([]); + manifestManager.commit = jest.fn().mockRejectedValue(new Error()); - expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); - expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory()); - expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( - lastComputed!.toEndpointFormat() + expect((await invokeCallback(manifestManager)).inputs[0]).toStrictEqual( + createNewEndpointPolicyInput({ + artifacts: {}, + manifest_version: '1.0.0', + schema_version: 'v1', + }) ); + + expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([ARTIFACT_EXCEPTIONS_MACOS]); + expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); }); - test('policy creation succeeds even if endpoint exception list creation fails', async () => { - const mockError = new Error('error creating endpoint list'); - const logger = loggingSystemMock.create().get('ingest_integration.test'); - const manifestManager = getManifestManagerMock(); - const lastComputed = await manifestManager.getLastComputedManifest(); - exceptionListClient.createEndpointList = jest.fn().mockRejectedValue(mockError); - const callback = getPackagePolicyCreateCallback( - logger, - manifestManager, - endpointAppContextMock.appClientFactory, - maxTimelineImportExportSize, - endpointAppContextMock.security, - endpointAppContextMock.alerts, - licenseService, - exceptionListClient + test('manifest is created successfuly when there is none', async () => { + const newManifest = Manifest.getDefault(); + newManifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + newManifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS); + + const manifestManager = buildManifestManagerMock(); + manifestManager.getLastComputedManifest = jest.fn().mockResolvedValue(null); + manifestManager.buildNewManifest = jest.fn().mockResolvedValue(newManifest); + manifestManager.pushArtifacts = jest.fn().mockResolvedValue([]); + manifestManager.commit = jest.fn().mockResolvedValue(null); + + expect((await invokeCallback(manifestManager)).inputs[0]).toStrictEqual( + createNewEndpointPolicyInput({ + artifacts: toArtifactRecords({ + [ARTIFACT_NAME_EXCEPTIONS_MACOS]: ARTIFACT_EXCEPTIONS_MACOS, + [ARTIFACT_NAME_TRUSTED_APPS_MACOS]: ARTIFACT_TRUSTED_APPS_MACOS, + }), + manifest_version: '1.0.0', + schema_version: 'v1', + }) ); - const policyConfig = createNewPackagePolicyMock(); - const newPolicyConfig = await callback(policyConfig, ctx, req); - expect(exceptionListClient.createEndpointList).toHaveBeenCalled(); - expect(newPolicyConfig.inputs[0]!.type).toEqual('endpoint'); - expect(newPolicyConfig.inputs[0]!.config!.policy.value).toEqual(policyFactory()); - expect(newPolicyConfig.inputs[0]!.config!.artifact_manifest.value).toEqual( - lastComputed!.toEndpointFormat() + expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([ + ARTIFACT_EXCEPTIONS_MACOS, + ARTIFACT_TRUSTED_APPS_MACOS, + ]); + expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); + }); + + test('policy is updated with only default entries from manifest', async () => { + const manifest = new Manifest({ soVersion: '1.0.1', semanticVersion: '1.0.1' }); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_WINDOWS); + + const manifestManager = buildManifestManagerMock(); + manifestManager.getLastComputedManifest = jest.fn().mockResolvedValue(manifest); + + expect((await invokeCallback(manifestManager)).inputs[0]).toStrictEqual( + createNewEndpointPolicyInput({ + artifacts: toArtifactRecords({ + [ARTIFACT_NAME_EXCEPTIONS_MACOS]: ARTIFACT_EXCEPTIONS_MACOS, + [ARTIFACT_NAME_TRUSTED_APPS_WINDOWS]: ARTIFACT_TRUSTED_APPS_WINDOWS, + }), + manifest_version: '1.0.1', + schema_version: 'v1', + }) ); + + expect(manifestManager.buildNewManifest).not.toHaveBeenCalled(); + expect(manifestManager.pushArtifacts).not.toHaveBeenCalled(); + expect(manifestManager.commit).not.toHaveBeenCalled(); }); }); - describe('when the license is below platinum', () => { + + describe('package policy update callback (when the license is below platinum)', () => { beforeEach(() => { licenseEmitter.next(Gold); // set license level to gold }); @@ -271,7 +256,8 @@ describe('ingest_integration tests ', () => { expect(updatedPolicyConfig.inputs[0]!.config!.policy.value).toEqual(mockPolicy); }); }); - describe('when the license is at least platinum', () => { + + describe('package policy update callback (when the license is at least platinum)', () => { beforeEach(() => { licenseEmitter.next(Platinum); // set license level to platinum }); diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts index 4dab1c305d17..080a8474da54 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts @@ -39,37 +39,18 @@ const getManifest = async (logger: Logger, manifestManager: ManifestManager): Pr if (manifest == null) { // New computed manifest based on current state of exception list const newManifest = await manifestManager.buildNewManifest(); - const diffs = newManifest.diff(Manifest.getDefault()); - - // Compress new artifacts - const adds = diffs.filter((diff) => diff.type === 'add').map((diff) => diff.id); - for (const artifactId of adds) { - const compressError = await newManifest.compressArtifact(artifactId); - if (compressError) { - throw compressError; - } - } // Persist new artifacts - const artifacts = adds - .map((artifactId) => newManifest.getArtifact(artifactId)) - .filter((artifact): artifact is InternalArtifactCompleteSchema => artifact !== undefined); - if (artifacts.length !== adds.length) { - throw new Error('Invalid artifact encountered.'); - } - const persistErrors = await manifestManager.pushArtifacts(artifacts); + const persistErrors = await manifestManager.pushArtifacts( + newManifest.getAllArtifacts() as InternalArtifactCompleteSchema[] + ); if (persistErrors.length) { reportErrors(logger, persistErrors); throw new Error('Unable to persist new artifacts.'); } // Commit the manifest state - if (diffs.length) { - const error = await manifestManager.commit(newManifest); - if (error) { - throw error; - } - } + await manifestManager.commit(newManifest); manifest = newManifest; } @@ -93,7 +74,7 @@ export const getPackagePolicyCreateCallback = ( licenseService: LicenseService, exceptionsClient: ExceptionListClient | undefined ): ExternalCallback[1] => { - const handlePackagePolicyCreate = async ( + return async ( newPackagePolicy: NewPackagePolicy, context: RequestHandlerContext, request: KibanaRequest @@ -143,7 +124,7 @@ export const getPackagePolicyCreateCallback = ( // Get most recent manifest const manifest = await getManifest(logger, manifestManager); - const serializedManifest = manifest.toEndpointFormat(); + const serializedManifest = manifest.toPackagePolicyManifest(); if (!manifestDispatchSchema.is(serializedManifest)) { // This should not happen. // But if it does, we log it and return it anyway. @@ -183,15 +164,13 @@ export const getPackagePolicyCreateCallback = ( return updatedPackagePolicy; }; - - return handlePackagePolicyCreate; }; export const getPackagePolicyUpdateCallback = ( logger: Logger, licenseService: LicenseService ): ExternalCallback[1] => { - const handlePackagePolicyUpdate = async ( + return async ( newPackagePolicy: NewPackagePolicy, context: RequestHandlerContext, request: KibanaRequest @@ -213,5 +192,4 @@ export const getPackagePolicyUpdateCallback = ( } return newPackagePolicy; }; - return handlePackagePolicyUpdate; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index c408cb56a7fd..6cc6a821eba3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -11,7 +11,6 @@ import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { validate } from '../../../../common/validate'; import { Entry, EntryNested } from '../../../../../lists/common/schemas/types'; -import { FoundExceptionListItemSchema } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema'; import { ExceptionListClient } from '../../../../../lists/server'; import { ENDPOINT_LIST_ID } from '../../../../common/shared_imports'; import { @@ -58,14 +57,14 @@ export async function maybeCompressArtifact( ): Promise { const compressedArtifact = { ...uncompressedArtifact }; if (internalArtifactCompleteSchema.is(uncompressedArtifact)) { - const compressedExceptionList = await compressExceptionList( + const compressedArtifactBody = await compressExceptionList( Buffer.from(uncompressedArtifact.body, 'base64') ); - compressedArtifact.body = compressedExceptionList.toString('base64'); - compressedArtifact.encodedSize = compressedExceptionList.byteLength; + compressedArtifact.body = compressedArtifactBody.toString('base64'); + compressedArtifact.encodedSize = compressedArtifactBody.byteLength; compressedArtifact.compressionAlgorithm = 'zlib'; compressedArtifact.encodedSha256 = createHash('sha256') - .update(compressedExceptionList) + .update(compressedArtifactBody) .digest('hex'); } return compressedArtifact; @@ -98,7 +97,7 @@ export async function getFullEndpointExceptionList( if (response?.data !== undefined) { exceptions.entries = exceptions.entries.concat( - translateToEndpointExceptions(response, schemaVersion) + translateToEndpointExceptions(response.data, schemaVersion) ); paging = (page - 1) * 100 + response.data.length < response.total; @@ -117,16 +116,17 @@ export async function getFullEndpointExceptionList( /** * Translates Exception list items to Exceptions the endpoint can understand - * @param exc + * @param exceptions + * @param schemaVersion */ export function translateToEndpointExceptions( - exc: FoundExceptionListItemSchema, + exceptions: ExceptionListItemSchema[], schemaVersion: string ): TranslatedExceptionListItem[] { const entrySet = new Set(); const entriesFiltered: TranslatedExceptionListItem[] = []; if (schemaVersion === 'v1') { - exc.data.forEach((entry) => { + exceptions.forEach((entry) => { const translatedItem = translateItem(schemaVersion, entry); const entryHash = createHash('sha256').update(JSON.stringify(translatedItem)).digest('hex'); if (!entrySet.has(entryHash)) { diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts index 047b79ea01ef..beaf0c06299f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts @@ -8,31 +8,48 @@ import { ManifestSchemaVersion } from '../../../../common/endpoint/schema/common'; import { InternalArtifactCompleteSchema } from '../../schemas'; import { getArtifactId } from './common'; -import { Manifest } from './manifest'; -import { - getMockArtifacts, - getMockManifest, - getMockManifestWithDiffs, - getEmptyMockManifest, -} from './mocks'; +import { isEmptyManifestDiff, Manifest } from './manifest'; +import { getMockArtifacts, toArtifactRecords } from './mocks'; describe('manifest', () => { - describe('Manifest object sanity checks', () => { - let artifacts: InternalArtifactCompleteSchema[] = []; - let manifest1: Manifest; - let manifest2: Manifest; - let emptyManifest: Manifest; + const TEST_POLICY_ID_1 = 'c6d16e42-c32d-4dce-8a88-113cfe276ad1'; + const TEST_POLICY_ID_2 = '93c46720-c217-11ea-9906-b5b8a21b268e'; + const ARTIFACT_ID_EXCEPTIONS_MACOS = + 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3'; + const ARTIFACT_ID_EXCEPTIONS_WINDOWS = + 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3'; + const ARTIFACT_ID_TRUSTED_APPS_MACOS = + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3'; + const ARTIFACT_ID_TRUSTED_APPS_WINDOWS = + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3'; - beforeAll(async () => { - artifacts = await getMockArtifacts({ compress: true }); - manifest1 = await getMockManifest({ compress: true }); - manifest2 = await getMockManifestWithDiffs({ compress: true }); - emptyManifest = await getEmptyMockManifest({ compress: true }); - }); + let ARTIFACTS: InternalArtifactCompleteSchema[] = []; + let ARTIFACTS_COPY: InternalArtifactCompleteSchema[] = []; + let ARTIFACT_EXCEPTIONS_MACOS: InternalArtifactCompleteSchema; + let ARTIFACT_EXCEPTIONS_WINDOWS: InternalArtifactCompleteSchema; + let ARTIFACT_TRUSTED_APPS_MACOS: InternalArtifactCompleteSchema; + let ARTIFACT_TRUSTED_APPS_WINDOWS: InternalArtifactCompleteSchema; + let ARTIFACT_COPY_EXCEPTIONS_MACOS: InternalArtifactCompleteSchema; + let ARTIFACT_COPY_EXCEPTIONS_WINDOWS: InternalArtifactCompleteSchema; + let ARTIFACT_COPY_TRUSTED_APPS_MACOS: InternalArtifactCompleteSchema; + let ARTIFACT_COPY_TRUSTED_APPS_WINDOWS: InternalArtifactCompleteSchema; + + beforeAll(async () => { + ARTIFACTS = await getMockArtifacts({ compress: true }); + ARTIFACTS_COPY = await getMockArtifacts({ compress: true }); + ARTIFACT_EXCEPTIONS_MACOS = ARTIFACTS[0]; + ARTIFACT_EXCEPTIONS_WINDOWS = ARTIFACTS[1]; + ARTIFACT_TRUSTED_APPS_MACOS = ARTIFACTS[2]; + ARTIFACT_TRUSTED_APPS_WINDOWS = ARTIFACTS[3]; + ARTIFACT_COPY_EXCEPTIONS_MACOS = ARTIFACTS_COPY[0]; + ARTIFACT_COPY_EXCEPTIONS_WINDOWS = ARTIFACTS_COPY[1]; + ARTIFACT_COPY_TRUSTED_APPS_MACOS = ARTIFACTS_COPY[2]; + ARTIFACT_COPY_TRUSTED_APPS_WINDOWS = ARTIFACTS_COPY[3]; + }); + describe('Manifest constructor', () => { test('Can create manifest with valid schema version', () => { - const manifest = new Manifest(); - expect(manifest).toBeInstanceOf(Manifest); + expect(new Manifest()).toBeInstanceOf(Manifest); }); test('Cannot create manifest with invalid schema version', () => { @@ -43,177 +60,638 @@ describe('manifest', () => { }).toThrow(); }); - test('Empty manifest transforms correctly to expected endpoint format', async () => { - expect(emptyManifest.toEndpointFormat()).toStrictEqual({ - artifacts: { - 'endpoint-exceptionlist-macos-v1': { - compression_algorithm: 'zlib', - encryption_algorithm: 'none', - decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - decoded_size: 14, - encoded_size: 22, - relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-exceptionlist-windows-v1': { - compression_algorithm: 'zlib', - encryption_algorithm: 'none', - decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - decoded_size: 14, - encoded_size: 22, - relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - }, + test('Can create manifest with valid constructor parameters', () => { + const manifest = new Manifest({ + schemaVersion: 'v1', + semanticVersion: '1.1.1', + soVersion: '2.2.2', + }); + + expect(manifest.getAllArtifacts()).toStrictEqual([]); + expect(manifest.getSchemaVersion()).toBe('v1'); + expect(manifest.getSemanticVersion()).toBe('1.1.1'); + expect(manifest.getSavedObjectVersion()).toBe('2.2.2'); + }); + }); + + describe('Manifest.getDefault()', () => { + test('Creates empty default manifest', () => { + const manifest = Manifest.getDefault(); + + expect(manifest.getAllArtifacts()).toStrictEqual([]); + expect(manifest.getSchemaVersion()).toBe('v1'); + expect(manifest.getSemanticVersion()).toBe('1.0.0'); + expect(manifest.getSavedObjectVersion()).toBe(undefined); + }); + }); + + describe('bumpSemanticVersion', () => { + test('Bumps the version properly', () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.1.1' }); + + manifest.bumpSemanticVersion(); + + expect(manifest.getSemanticVersion()).toBe('1.1.2'); + }); + }); + + describe('addEntry', () => { + test('Adds default artifact', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + + expect(manifest.getAllArtifacts()).toStrictEqual([ARTIFACT_EXCEPTIONS_MACOS]); + expect(manifest.isDefaultArtifact(ARTIFACT_EXCEPTIONS_MACOS)).toBe(true); + expect(manifest.getArtifactTargetPolicies(ARTIFACT_EXCEPTIONS_MACOS)).toStrictEqual( + new Set() + ); + }); + + test('Adds policy specific artifact', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + + expect(manifest.getAllArtifacts()).toStrictEqual([ARTIFACT_EXCEPTIONS_MACOS]); + expect(manifest.isDefaultArtifact(ARTIFACT_EXCEPTIONS_MACOS)).toBe(false); + expect(manifest.getArtifactTargetPolicies(ARTIFACT_EXCEPTIONS_MACOS)).toStrictEqual( + new Set([TEST_POLICY_ID_1]) + ); + }); + + test('Adds same artifact as default and policy specific', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + + expect(manifest.getAllArtifacts()).toStrictEqual([ARTIFACT_EXCEPTIONS_MACOS]); + expect(manifest.isDefaultArtifact(ARTIFACT_EXCEPTIONS_MACOS)).toBe(true); + expect(manifest.getArtifactTargetPolicies(ARTIFACT_EXCEPTIONS_MACOS)).toStrictEqual( + new Set([TEST_POLICY_ID_1]) + ); + }); + + test('Adds multiple artifacts as default and policy specific', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_2); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_WINDOWS); + + expect(manifest.getAllArtifacts()).toStrictEqual(ARTIFACTS.slice(0, 4)); + expect(manifest.isDefaultArtifact(ARTIFACT_EXCEPTIONS_MACOS)).toBe(true); + expect(manifest.isDefaultArtifact(ARTIFACT_EXCEPTIONS_WINDOWS)).toBe(false); + expect(manifest.isDefaultArtifact(ARTIFACT_TRUSTED_APPS_MACOS)).toBe(true); + expect(manifest.isDefaultArtifact(ARTIFACT_TRUSTED_APPS_WINDOWS)).toBe(true); + expect(manifest.getArtifactTargetPolicies(ARTIFACT_EXCEPTIONS_MACOS)).toStrictEqual( + new Set([TEST_POLICY_ID_1]) + ); + expect(manifest.getArtifactTargetPolicies(ARTIFACT_EXCEPTIONS_WINDOWS)).toStrictEqual( + new Set([TEST_POLICY_ID_2]) + ); + expect(manifest.getArtifactTargetPolicies(ARTIFACT_TRUSTED_APPS_MACOS)).toStrictEqual( + new Set([TEST_POLICY_ID_1, TEST_POLICY_ID_2]) + ); + expect(manifest.getArtifactTargetPolicies(ARTIFACT_TRUSTED_APPS_WINDOWS)).toStrictEqual( + new Set([]) + ); + }); + + test('Adding same artifact as default multiple times has no effect', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + + expect(manifest.getAllArtifacts()).toStrictEqual(ARTIFACTS.slice(0, 1)); + expect(manifest.isDefaultArtifact(ARTIFACT_EXCEPTIONS_MACOS)).toBe(true); + expect(manifest.getArtifactTargetPolicies(ARTIFACT_EXCEPTIONS_MACOS)).toStrictEqual( + new Set([]) + ); + }); + + test('Adding same artifact as policy specific for same policy multiple times has no effect', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + + expect(manifest.getAllArtifacts()).toStrictEqual(ARTIFACTS.slice(0, 1)); + expect(manifest.isDefaultArtifact(ARTIFACT_EXCEPTIONS_MACOS)).toBe(false); + expect(manifest.getArtifactTargetPolicies(ARTIFACT_EXCEPTIONS_MACOS)).toStrictEqual( + new Set([TEST_POLICY_ID_1]) + ); + }); + }); + + describe('getAllArtifacts', () => { + test('Returns empty list initially', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + expect(manifest.getAllArtifacts()).toStrictEqual([]); + }); + + test('Returns only unique artifacts', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_2); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS); + + expect(manifest.getAllArtifacts()).toStrictEqual(ARTIFACTS.slice(0, 2)); + }); + }); + + describe('getArtifact', () => { + test('Returns undefined for non existing artifact id', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + + expect(manifest.getArtifact('non-existing-artifact-macos-v1')).toBeUndefined(); + }); + + test('Returns default artifact', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS); + + expect(manifest.getArtifact(getArtifactId(ARTIFACT_EXCEPTIONS_MACOS))).toStrictEqual( + ARTIFACT_EXCEPTIONS_MACOS + ); + }); + + test('Returns policy specific artifact', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_2); + + expect(manifest.getArtifact(getArtifactId(ARTIFACT_EXCEPTIONS_MACOS))).toStrictEqual( + ARTIFACT_EXCEPTIONS_MACOS + ); + }); + }); + + describe('containsArtifact', () => { + test('Returns false for artifact that is not in the manifest', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + + expect(manifest.containsArtifact(ARTIFACT_EXCEPTIONS_WINDOWS)).toBe(false); + }); + + test('Returns true for default artifact that is in the manifest', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS); + + expect(manifest.containsArtifact(ARTIFACT_EXCEPTIONS_WINDOWS)).toBe(true); + }); + + test('Returns true for policy specific artifact that is in the manifest', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_2); + + expect(manifest.containsArtifact(ARTIFACT_EXCEPTIONS_WINDOWS)).toBe(true); + }); + + test('Returns true for different instances but same ids', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + + expect(manifest.containsArtifact(ARTIFACT_COPY_EXCEPTIONS_MACOS)).toBe(true); + }); + }); + + describe('isDefaultArtifact', () => { + test('Returns undefined for artifact that is not in the manifest', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + + expect(manifest.isDefaultArtifact(ARTIFACT_EXCEPTIONS_WINDOWS)).toBeUndefined(); + }); + + test('Returns true for default artifact that is in the manifest', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + + expect(manifest.isDefaultArtifact(ARTIFACT_EXCEPTIONS_MACOS)).toBe(true); + }); + + test('Returns false for policy specific artifact that is in the manifest', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + + expect(manifest.isDefaultArtifact(ARTIFACT_EXCEPTIONS_MACOS)).toBe(false); + }); + + test('Returns true for different instances but same ids', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + + expect(manifest.isDefaultArtifact(ARTIFACT_COPY_EXCEPTIONS_MACOS)).toBe(true); + }); + }); + + describe('getArtifactTargetPolicies', () => { + test('Returns undefined for artifact that is not in the manifest', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + + expect(manifest.getArtifactTargetPolicies(ARTIFACT_EXCEPTIONS_WINDOWS)).toBeUndefined(); + }); + + test('Returns empty set for default artifact that is in the manifest', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + + expect(manifest.getArtifactTargetPolicies(ARTIFACT_EXCEPTIONS_MACOS)).toStrictEqual( + new Set() + ); + }); + + test('Returns policy set for policy specific artifact that is in the manifest', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_2); + + expect(manifest.getArtifactTargetPolicies(ARTIFACT_EXCEPTIONS_MACOS)).toStrictEqual( + new Set([TEST_POLICY_ID_1, TEST_POLICY_ID_2]) + ); + }); + + test('Returns policy set for different instances but same ids', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_2); + + expect(manifest.getArtifactTargetPolicies(ARTIFACT_COPY_EXCEPTIONS_MACOS)).toStrictEqual( + new Set([TEST_POLICY_ID_1, TEST_POLICY_ID_2]) + ); + }); + }); + + describe('diff', () => { + test('Returns empty diff between empty manifests', async () => { + expect(Manifest.getDefault().diff(Manifest.getDefault())).toStrictEqual({ + additions: [], + removals: [], + transitions: [], + }); + }); + + test('Returns diff from empty manifest', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.1' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + + expect(manifest.diff(Manifest.getDefault())).toStrictEqual({ + additions: ARTIFACTS.slice(0, 3), + removals: [], + transitions: [], + }); + }); + + test('Returns empty diff for equal manifests', async () => { + const manifest1 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest1.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest1.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest1.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + manifest1.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + manifest1.addEntry(ARTIFACT_TRUSTED_APPS_WINDOWS); + manifest1.addEntry(ARTIFACT_TRUSTED_APPS_WINDOWS, TEST_POLICY_ID_2); + + const manifest2 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.1' }); + + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_MACOS); + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest2.addEntry(ARTIFACT_COPY_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + manifest2.addEntry(ARTIFACT_COPY_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + manifest2.addEntry(ARTIFACT_COPY_TRUSTED_APPS_WINDOWS); + manifest2.addEntry(ARTIFACT_COPY_TRUSTED_APPS_WINDOWS, TEST_POLICY_ID_2); + + expect(manifest2.diff(manifest1)).toStrictEqual({ + additions: [], + removals: [], + transitions: [], + }); + }); + + test('Returns additions diff properly', async () => { + const manifest1 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest1.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest1.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + + const manifest2 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.1' }); + + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_MACOS); + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest2.addEntry(ARTIFACT_COPY_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + manifest2.addEntry(ARTIFACT_COPY_TRUSTED_APPS_WINDOWS); + + expect(manifest2.diff(manifest1)).toStrictEqual({ + additions: [ARTIFACT_COPY_TRUSTED_APPS_MACOS, ARTIFACT_COPY_TRUSTED_APPS_WINDOWS], + removals: [], + transitions: [], + }); + }); + + test('Returns removals diff properly', async () => { + const manifest1 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest1.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest1.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest1.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + manifest1.addEntry(ARTIFACT_TRUSTED_APPS_WINDOWS); + + const manifest2 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.1' }); + + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_MACOS); + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + + expect(manifest2.diff(manifest1)).toStrictEqual({ + additions: [], + removals: [ARTIFACT_COPY_TRUSTED_APPS_MACOS, ARTIFACT_COPY_TRUSTED_APPS_WINDOWS], + transitions: [], + }); + }); + + test('Returns transitions from one policy to another in diff properly', async () => { + const manifest1 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest1.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest1.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest1.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + + const manifest2 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.1' }); + + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_MACOS); + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + // policy transition + manifest2.addEntry(ARTIFACT_COPY_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + + expect(manifest2.diff(manifest1)).toStrictEqual({ + additions: [], + removals: [], + transitions: [ARTIFACT_COPY_TRUSTED_APPS_MACOS], + }); + }); + + test('Returns transitions from policy to default in diff properly', async () => { + const manifest1 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest1.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest1.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest1.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + + const manifest2 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.1' }); + + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_MACOS); + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + // transition to default + manifest2.addEntry(ARTIFACT_COPY_TRUSTED_APPS_MACOS); + + expect(manifest2.diff(manifest1)).toStrictEqual({ + additions: [], + removals: [], + transitions: [ARTIFACT_COPY_TRUSTED_APPS_MACOS], + }); + }); + + test('Returns transitions from default to specific policy in diff properly', async () => { + const manifest1 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest1.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest1.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest1.addEntry(ARTIFACT_TRUSTED_APPS_MACOS); + + const manifest2 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.1' }); + + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_MACOS); + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + // transition to specific policy + manifest2.addEntry(ARTIFACT_COPY_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + + expect(manifest2.diff(manifest1)).toStrictEqual({ + additions: [], + removals: [], + transitions: [ARTIFACT_COPY_TRUSTED_APPS_MACOS], + }); + }); + + test('Returns complex transitions diff properly', async () => { + const manifest1 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest1.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest1.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest1.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest1.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + manifest1.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + + const manifest2 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.1' }); + + // transition to default policy only + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_MACOS); + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + // transition to second policy + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_2); + // transition to one policy only + manifest2.addEntry(ARTIFACT_COPY_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + + expect(manifest2.diff(manifest1)).toStrictEqual({ + additions: [], + removals: [], + transitions: ARTIFACTS_COPY.slice(0, 3), + }); + }); + + test('Returns complex diff properly', async () => { + const manifest1 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest1.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest1.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest1.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + manifest1.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + manifest1.addEntry(ARTIFACT_TRUSTED_APPS_WINDOWS); + + const manifest2 = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.1' }); + + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_MACOS); + manifest2.addEntry(ARTIFACT_COPY_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest2.addEntry(ARTIFACT_COPY_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + + expect(manifest2.diff(manifest1)).toStrictEqual({ + additions: [ARTIFACT_COPY_EXCEPTIONS_WINDOWS], + removals: [ARTIFACT_TRUSTED_APPS_WINDOWS], + transitions: [ARTIFACT_COPY_EXCEPTIONS_MACOS, ARTIFACT_COPY_TRUSTED_APPS_MACOS], + }); + }); + }); + + describe('toPackagePolicyManifest', () => { + test('Returns empty manifest', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + expect(manifest.toPackagePolicyManifest()).toStrictEqual({ + schema_version: 'v1', manifest_version: '1.0.0', + artifacts: {}, + }); + }); + + test('Returns default policy manifest when no policy id provided', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + + expect(manifest.toPackagePolicyManifest()).toStrictEqual({ schema_version: 'v1', + manifest_version: '1.0.0', + artifacts: toArtifactRecords({ + 'endpoint-exceptionlist-windows-v1': ARTIFACT_EXCEPTIONS_MACOS, + 'endpoint-exceptionlist-macos-v1': ARTIFACT_EXCEPTIONS_WINDOWS, + }), }); }); - test('Manifest transforms correctly to expected endpoint format', async () => { - expect(manifest1.toEndpointFormat()).toStrictEqual({ - artifacts: { - 'endpoint-exceptionlist-macos-v1': { - compression_algorithm: 'zlib', - encryption_algorithm: 'none', - decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', - decoded_size: 432, - encoded_size: 147, - relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - }, - 'endpoint-exceptionlist-windows-v1': { - compression_algorithm: 'zlib', - encryption_algorithm: 'none', - decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', - decoded_size: 432, - encoded_size: 147, - relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - }, - 'endpoint-trustlist-linux-v1': { - compression_algorithm: 'zlib', - decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - decoded_size: 432, - encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', - encoded_size: 147, - encryption_algorithm: 'none', - relative_url: - '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - }, - 'endpoint-trustlist-macos-v1': { - compression_algorithm: 'zlib', - decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - decoded_size: 432, - encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', - encoded_size: 147, - encryption_algorithm: 'none', - relative_url: - '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - }, - 'endpoint-trustlist-windows-v1': { - compression_algorithm: 'zlib', - decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - decoded_size: 432, - encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', - encoded_size: 147, - encryption_algorithm: 'none', - relative_url: - '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - }, - }, + test('Returns default policy manifest when no policy specific artifacts present', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + + expect(manifest.toPackagePolicyManifest(TEST_POLICY_ID_2)).toStrictEqual({ + schema_version: 'v1', manifest_version: '1.0.0', + artifacts: toArtifactRecords({ + 'endpoint-exceptionlist-windows-v1': ARTIFACT_EXCEPTIONS_MACOS, + 'endpoint-exceptionlist-macos-v1': ARTIFACT_EXCEPTIONS_WINDOWS, + }), + }); + }); + + test('Returns policy specific manifest when policy specific artifacts present', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + + expect(manifest.toPackagePolicyManifest(TEST_POLICY_ID_2)).toStrictEqual({ schema_version: 'v1', + manifest_version: '1.0.0', + artifacts: toArtifactRecords({ + 'endpoint-exceptionlist-windows-v1': ARTIFACT_TRUSTED_APPS_MACOS, + 'endpoint-exceptionlist-macos-v1': ARTIFACT_EXCEPTIONS_WINDOWS, + }), }); }); + }); + + describe('toSavedObject', () => { + test('Returns empty saved object', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + expect(manifest.toSavedObject()).toStrictEqual({ + schemaVersion: 'v1', + semanticVersion: '1.0.0', + artifacts: [], + }); + }); + + test('Returns populated saved object', async () => { + const manifest = new Manifest({ schemaVersion: 'v1', semanticVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_WINDOWS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_WINDOWS, TEST_POLICY_ID_2); - test('Manifest transforms correctly to expected saved object format', async () => { - expect(manifest1.toSavedObject()).toStrictEqual({ + expect(manifest.toSavedObject()).toStrictEqual({ schemaVersion: 'v1', semanticVersion: '1.0.0', - ids: [ - 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + artifacts: [ + { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_WINDOWS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_WINDOWS, policyId: TEST_POLICY_ID_2 }, ], }); }); + }); - test('Manifest returns diffs since supplied manifest', async () => { - const diffs = manifest2.diff(manifest1); - expect(diffs).toEqual([ - { - id: - 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - type: 'delete', - }, - { - id: - 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - type: 'delete', - }, - { - id: - 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - type: 'delete', - }, - { - id: - 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - type: 'delete', - }, - { - id: - 'endpoint-exceptionlist-macos-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', - type: 'add', - }, - ]); - }); - - test('Manifest returns data for given artifact', async () => { - const artifact = artifacts[0]; - const returned = manifest1.getArtifact(getArtifactId(artifact)); - expect(returned).toEqual(artifact); - }); - - test('Manifest returns entries map', async () => { - const entries = manifest1.getEntries(); - const keys = Object.keys(entries); - expect(keys).toEqual([ - 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - ]); - }); - - test('Manifest returns true if contains artifact', async () => { - const found = manifest1.contains( - 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' - ); - expect(found).toEqual(true); - }); - - test('Manifest can be created from list of artifacts', async () => { - const oldManifest = new Manifest(); - const manifest = Manifest.fromArtifacts(artifacts, oldManifest); - expect( - manifest.contains( - 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' - ) - ).toEqual(true); - expect( - manifest.contains( - 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3' - ) - ).toEqual(true); + describe('isEmptyManifestDiff', () => { + test('Returns true when no additions, removals or transitions', async () => { + expect(isEmptyManifestDiff({ additions: [], removals: [], transitions: [] })).toBe(true); + }); + + test('Returns false when there are additions', async () => { + const diff = { additions: [ARTIFACT_EXCEPTIONS_MACOS], removals: [], transitions: [] }; + + expect(isEmptyManifestDiff(diff)).toBe(false); + }); + + test('Returns false when there are removals', async () => { + const diff = { additions: [], removals: [ARTIFACT_EXCEPTIONS_MACOS], transitions: [] }; + + expect(isEmptyManifestDiff(diff)).toBe(false); + }); + + test('Returns false when there are transitions', async () => { + const diff = { additions: [], removals: [], transitions: [ARTIFACT_EXCEPTIONS_MACOS] }; + + expect(isEmptyManifestDiff(diff)).toBe(false); + }); + + test('Returns false when there are all typesof changes', async () => { + const diff = { + additions: [ARTIFACT_EXCEPTIONS_MACOS], + removals: [ARTIFACT_EXCEPTIONS_WINDOWS], + transitions: [ARTIFACT_TRUSTED_APPS_MACOS], + }; + + expect(isEmptyManifestDiff(diff)).toBe(false); }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts index e2065b6bbc37..7e1accac37cf 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts @@ -5,13 +5,13 @@ * 2.0. */ +import { flatMap, isEqual } from 'lodash'; import semver from 'semver'; -import { validate } from '../../../../common/validate'; +import { validate } from '../../../../common'; import { InternalArtifactSchema, InternalManifestSchema, - internalArtifactCompleteSchema, - InternalArtifactCompleteSchema, + InternalManifestEntrySchema, } from '../../schemas/artifacts'; import { ManifestSchemaVersion, @@ -20,21 +20,46 @@ import { } from '../../../../common/endpoint/schema/common'; import { manifestSchema, ManifestSchema } from '../../../../common/endpoint/schema/manifest'; import { ManifestEntry } from './manifest_entry'; -import { maybeCompressArtifact, isCompressed } from './lists'; import { getArtifactId } from './common'; import { ManifestVersion, manifestVersion } from '../../schemas/artifacts/manifest'; +function createInternalManifestEntries( + artifactIds: string[], + policyId?: string +): InternalManifestEntrySchema[] { + return artifactIds.map((artifactId) => ({ policyId, artifactId })); +} + export interface ManifestDiff { - type: string; - id: string; + additions: InternalArtifactSchema[]; + removals: InternalArtifactSchema[]; + transitions: InternalArtifactSchema[]; +} + +export function isEmptyManifestDiff(diff: ManifestDiff) { + return diff.additions.length === 0 && diff.removals.length === 0 && diff.transitions.length === 0; +} + +interface ManifestEntryDescriptor { + isDefaultEntry: boolean; + specificTargetPolicies: Set; + entry: ManifestEntry; +} + +function addValueToSet(set?: Set, value?: T) { + return new Set([...(set?.values() || []), ...(value !== undefined ? [value] : [])]); } export class Manifest { - private entries: Record; + private readonly allEntries: Map; + private readonly defaultEntries: Map; + private readonly policySpecificEntries: Map>; private version: ManifestVersion; constructor(version?: Partial) { - this.entries = {}; + this.allEntries = new Map(); + this.defaultEntries = new Map(); + this.policySpecificEntries = new Map(); const decodedVersion = { schemaVersion: version?.schemaVersion ?? 'v1', @@ -54,28 +79,6 @@ export class Manifest { return new Manifest({ schemaVersion, semanticVersion: '1.0.0' }); } - public static fromArtifacts( - artifacts: InternalArtifactCompleteSchema[], - oldManifest: Manifest, - schemaVersion?: ManifestSchemaVersion - ): Manifest { - const manifest = new Manifest({ - schemaVersion, - semanticVersion: oldManifest.getSemanticVersion(), - soVersion: oldManifest.getSavedObjectVersion(), - }); - artifacts.forEach((artifact) => { - const id = getArtifactId(artifact); - const existingArtifact = oldManifest.getArtifact(id); - if (existingArtifact) { - manifest.addEntry(existingArtifact); - } else { - manifest.addEntry(artifact); - } - }); - return manifest; - } - public bumpSemanticVersion() { const newSemanticVersion = semver.inc(this.getSemanticVersion(), 'patch'); if (!semanticVersion.is(newSemanticVersion)) { @@ -84,26 +87,6 @@ export class Manifest { this.version.semanticVersion = newSemanticVersion; } - public async compressArtifact(id: string): Promise { - try { - const artifact = this.getArtifact(id); - if (artifact == null) { - throw new Error(`Corrupted manifest detected. Artifact ${id} not in manifest.`); - } - - const compressedArtifact = await maybeCompressArtifact(artifact); - if (!isCompressed(compressedArtifact)) { - throw new Error(`Unable to compress artifact: ${id}`); - } else if (!internalArtifactCompleteSchema.is(compressedArtifact)) { - throw new Error(`Incomplete artifact detected: ${id}`); - } - this.addEntry(compressedArtifact); - } catch (err) { - return err; - } - return null; - } - public getSchemaVersion(): ManifestSchemaVersion { return this.version.schemaVersion; } @@ -116,53 +99,85 @@ export class Manifest { return this.version.semanticVersion; } - public addEntry(artifact: InternalArtifactSchema) { - const entry = new ManifestEntry(artifact); - this.entries[entry.getDocId()] = entry; + public addEntry(artifact: InternalArtifactSchema, policyId?: string) { + const existingDescriptor = this.allEntries.get(getArtifactId(artifact)); + const descriptor = { + isDefaultEntry: existingDescriptor?.isDefaultEntry || policyId === undefined, + specificTargetPolicies: addValueToSet(existingDescriptor?.specificTargetPolicies, policyId), + entry: existingDescriptor?.entry || new ManifestEntry(artifact), + }; + + this.allEntries.set(descriptor.entry.getDocId(), descriptor); + + if (policyId) { + const entries = this.policySpecificEntries.get(policyId) || new Map(); + entries.set(descriptor.entry.getDocId(), descriptor.entry); + + this.policySpecificEntries.set(policyId, entries); + } else { + this.defaultEntries.set(descriptor.entry.getDocId(), descriptor.entry); + } } - public contains(artifactId: string): boolean { - return artifactId in this.entries; + public getAllArtifacts(): InternalArtifactSchema[] { + return [...this.allEntries.values()].map((descriptor) => descriptor.entry.getArtifact()); } - public getEntries(): Record { - return this.entries; + public getArtifact(artifactId: string): InternalArtifactSchema | undefined { + return this.allEntries.get(artifactId)?.entry.getArtifact(); } - public getEntry(artifactId: string): ManifestEntry | undefined { - return this.entries[artifactId]; + public containsArtifact(artifact: InternalArtifactSchema): boolean { + return this.allEntries.has(getArtifactId(artifact)); } - public getArtifact(artifactId: string): InternalArtifactSchema | undefined { - return this.getEntry(artifactId)?.getArtifact(); + public isDefaultArtifact(artifact: InternalArtifactSchema): boolean | undefined { + return this.allEntries.get(getArtifactId(artifact))?.isDefaultEntry; } - public diff(manifest: Manifest): ManifestDiff[] { - const diffs: ManifestDiff[] = []; + public getArtifactTargetPolicies(artifact: InternalArtifactSchema): Set | undefined { + return this.allEntries.get(getArtifactId(artifact))?.specificTargetPolicies; + } + + public diff(manifest: Manifest): ManifestDiff { + const diff: ManifestDiff = { + additions: [], + removals: [], + transitions: [], + }; - for (const id in manifest.getEntries()) { - if (!this.contains(id)) { - diffs.push({ type: 'delete', id }); + for (const artifact of manifest.getAllArtifacts()) { + if (!this.containsArtifact(artifact)) { + diff.removals.push(artifact); + } else if ( + this.isDefaultArtifact(artifact) !== manifest.isDefaultArtifact(artifact) || + !isEqual( + this.getArtifactTargetPolicies(artifact), + manifest.getArtifactTargetPolicies(artifact) + ) + ) { + diff.transitions.push(artifact); } } - for (const id in this.entries) { - if (!manifest.contains(id)) { - diffs.push({ type: 'add', id }); + for (const artifact of this.getAllArtifacts()) { + if (!manifest.containsArtifact(artifact)) { + diff.additions.push(artifact); } } - return diffs; + return diff; } - public toEndpointFormat(): ManifestSchema { + public toPackagePolicyManifest(policyId?: string): ManifestSchema { + const entries = (!!policyId && this.policySpecificEntries.get(policyId)) || this.defaultEntries; const manifestObj: ManifestSchema = { manifest_version: this.getSemanticVersion(), schema_version: this.getSchemaVersion(), artifacts: {}, }; - for (const entry of Object.values(this.entries)) { + for (const entry of entries.values()) { manifestObj.artifacts[entry.getIdentifier()] = entry.getRecord(); } @@ -176,7 +191,15 @@ export class Manifest { public toSavedObject(): InternalManifestSchema { return { - ids: Object.keys(this.getEntries()), + artifacts: [ + ...createInternalManifestEntries([...this.defaultEntries.keys()]), + ...flatMap([...this.policySpecificEntries.keys()], (policyId) => + createInternalManifestEntries( + [...(this.policySpecificEntries.get(policyId)?.keys() || [])], + policyId + ) + ), + ], schemaVersion: this.getSchemaVersion(), semanticVersion: this.getSemanticVersion(), }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrations.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrations.test.ts new file mode 100644 index 000000000000..814a9880014c --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrations.test.ts @@ -0,0 +1,57 @@ +/* + * 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 { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { migrationMocks } from 'src/core/server/mocks'; +import { ManifestConstants } from './common'; +import { migrations, OldInternalManifestSchema } from './migrations'; + +describe('7.12.0 manifest migrations', () => { + const ARTIFACT_ID_0 = + 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3'; + const ARTIFACT_ID_1 = + 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3'; + const ARTIFACT_ID_2 = + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3'; + const ARTIFACT_ID_3 = + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3'; + + const migration = migrations['7.12.0']; + + test('Migrates ids property', () => { + const doc: SavedObjectUnsanitizedDoc = { + attributes: { + ids: [ARTIFACT_ID_0, ARTIFACT_ID_1, ARTIFACT_ID_2, ARTIFACT_ID_3], + schemaVersion: 'v1', + semanticVersion: '1.0.1', + }, + id: 'endpoint-manifest-v1', + migrationVersion: {}, + references: [], + type: ManifestConstants.SAVED_OBJECT_TYPE, + updated_at: '2020-06-09T20:18:20.349Z', + }; + + expect(migration(doc, migrationMocks.createContext())).toStrictEqual({ + attributes: { + artifacts: [ + { artifactId: ARTIFACT_ID_0, policyId: undefined }, + { artifactId: ARTIFACT_ID_1, policyId: undefined }, + { artifactId: ARTIFACT_ID_2, policyId: undefined }, + { artifactId: ARTIFACT_ID_3, policyId: undefined }, + ], + schemaVersion: 'v1', + semanticVersion: '1.0.1', + }, + id: 'endpoint-manifest-v1', + migrationVersion: {}, + references: [], + type: ManifestConstants.SAVED_OBJECT_TYPE, + updated_at: '2020-06-09T20:18:20.349Z', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrations.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrations.ts new file mode 100644 index 000000000000..e419c4297b23 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrations.ts @@ -0,0 +1,35 @@ +/* + * 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 { + SavedObjectMigrationMap, + SavedObjectSanitizedDoc, + SavedObjectUnsanitizedDoc, +} from 'kibana/server'; + +import { InternalManifestSchema } from '../../schemas/artifacts'; + +export type OldInternalManifestSchema = Omit & { + ids: string[]; +}; + +export const migrations: SavedObjectMigrationMap = { + '7.12.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + const { ids, ...rest } = doc.attributes; + + return { + ...doc, + references: doc.references || [], + attributes: { + ...rest, + artifacts: (ids || []).map((artifactId) => ({ artifactId, policyId: undefined })), + }, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts index 738a995f9fc6..1a582a51c52c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { PackagePolicy } from '../../../../../fleet/common'; +import { mapValues } from 'lodash'; +import { PackagePolicy, PackagePolicyConfigRecord } from '../../../../../fleet/common'; import { createPackagePolicyMock } from '../../../../../fleet/common/mocks'; import { InternalArtifactCompleteSchema } from '../../schemas/artifacts'; import { @@ -63,83 +64,94 @@ export const getMockManifest = async (opts?: { compress: boolean }) => { return manifest; }; -export const getMockManifestWithDiffs = async (opts?: { compress: boolean }) => { - const manifest = new Manifest(); - const artifacts = await getMockArtifactsWithDiff(opts); - artifacts.forEach((artifact) => manifest.addEntry(artifact)); - return manifest; -}; +const toArtifactRecord = (artifactName: string, artifact: InternalArtifactCompleteSchema) => ({ + compression_algorithm: artifact.compressionAlgorithm, + decoded_sha256: artifact.decodedSha256, + decoded_size: artifact.decodedSize, + encoded_sha256: artifact.encodedSha256, + encoded_size: artifact.encodedSize, + encryption_algorithm: artifact.encryptionAlgorithm, + relative_url: `/api/endpoint/artifacts/download/${artifactName}/${artifact.decodedSha256}`, +}); -export const getEmptyMockManifest = async (opts?: { compress: boolean }) => { - const manifest = new Manifest(); - const artifacts = await getEmptyMockArtifacts(opts); - artifacts.forEach((artifact) => manifest.addEntry(artifact)); - return manifest; +export const toArtifactRecords = (artifacts: Record) => + mapValues(artifacts, (artifact, key) => toArtifactRecord(key, artifact)); + +export const createPackagePolicyWithConfigMock = ( + options: Partial & { config?: PackagePolicyConfigRecord } +): PackagePolicy => { + const { config, ...packagePolicyOverrides } = options; + const packagePolicy = createPackagePolicyMock(); + packagePolicy.inputs[0].config = options.config; + return { ...packagePolicy, ...packagePolicyOverrides }; }; export const createPackagePolicyWithInitialManifestMock = (): PackagePolicy => { - const packagePolicy = createPackagePolicyMock(); - packagePolicy.inputs[0].config!.artifact_manifest = { - value: { - artifacts: { - 'endpoint-exceptionlist-macos-v1': { - compression_algorithm: 'zlib', - encryption_algorithm: 'none', - decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - decoded_size: 14, - encoded_size: 22, - relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - }, - 'endpoint-exceptionlist-windows-v1': { - compression_algorithm: 'zlib', - encryption_algorithm: 'none', - decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', - decoded_size: 14, - encoded_size: 22, - relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + return createPackagePolicyWithConfigMock({ + config: { + artifact_manifest: { + value: { + artifacts: { + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decoded_size: 14, + encoded_size: 22, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decoded_size: 14, + encoded_size: 22, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + }, + manifest_version: '1.0.0', + schema_version: 'v1', }, }, - manifest_version: '1.0.0', - schema_version: 'v1', }, - }; - return packagePolicy; + }); }; export const createPackagePolicyWithManifestMock = (): PackagePolicy => { - const packagePolicy = createPackagePolicyMock(); - packagePolicy.inputs[0].config!.artifact_manifest = { - value: { - artifacts: { - 'endpoint-exceptionlist-macos-v1': { - compression_algorithm: 'zlib', - encryption_algorithm: 'none', - decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', - decoded_size: 432, - encoded_size: 147, - relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - }, - 'endpoint-exceptionlist-windows-v1': { - compression_algorithm: 'zlib', - encryption_algorithm: 'none', - decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', - decoded_size: 432, - encoded_size: 147, - relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + return createPackagePolicyWithConfigMock({ + config: { + artifact_manifest: { + value: { + artifacts: { + 'endpoint-exceptionlist-macos-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, + 'endpoint-exceptionlist-windows-v1': { + compression_algorithm: 'zlib', + encryption_algorithm: 'none', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + decoded_size: 432, + encoded_size: 147, + relative_url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, + }, + manifest_version: '1.0.1', + schema_version: 'v1', }, }, - manifest_version: '1.0.1', - schema_version: 'v1', }, - }; - - return packagePolicy; + }); }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts index 8596e6b9917a..2202336ef451 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts @@ -8,6 +8,7 @@ import { SavedObjectsType } from '../../../../../../../src/core/server'; import { ArtifactConstants, ManifestConstants } from './common'; +import { migrations } from './migrations'; export const exceptionsArtifactSavedObjectType = ArtifactConstants.SAVED_OBJECT_TYPE; export const manifestSavedObjectType = ManifestConstants.SAVED_OBJECT_TYPE; @@ -63,9 +64,18 @@ export const manifestSavedObjectMappings: SavedObjectsType['mappings'] = { type: 'keyword', index: false, }, - ids: { - type: 'keyword', - index: false, + artifacts: { + type: 'nested', + properties: { + policyId: { + type: 'keyword', + index: false, + }, + artifactId: { + type: 'keyword', + index: false, + }, + }, }, }, }; @@ -82,4 +92,5 @@ export const manifestType: SavedObjectsType = { hidden: false, namespaceType: 'agnostic', mappings: manifestSavedObjectMappings, + migrations, }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts index 08f835951801..9fac617f1f06 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts @@ -12,8 +12,27 @@ import { createMockEndpointAppContext } from '../../mocks'; import { ManifestTaskConstants, ManifestTask } from './task'; import { MockManifestTask } from './task.mock'; +import { ManifestManager } from '../../services/artifacts/manifest_manager'; +import { buildManifestManagerMock } from '../../services/artifacts/manifest_manager/manifest_manager.mock'; +import { InternalArtifactCompleteSchema } from '../../schemas/artifacts'; +import { getMockArtifacts } from './mocks'; +import { Manifest } from './manifest'; describe('task', () => { + const MOCK_TASK_INSTANCE = { + id: `${ManifestTaskConstants.TYPE}:1.0.0`, + runAt: new Date(), + attempts: 0, + ownerId: '', + status: TaskStatus.Running, + startedAt: new Date(), + scheduledAt: new Date(), + retryAt: new Date(), + params: {}, + state: {}, + taskType: ManifestTaskConstants.TYPE, + }; + describe('Periodic task sanity checks', () => { test('can create task', () => { const manifestTask = new ManifestTask({ @@ -50,25 +69,255 @@ describe('task', () => { endpointAppContext: mockContext, taskManager: mockTaskManager, }); - const mockTaskInstance = { - id: ManifestTaskConstants.TYPE, - runAt: new Date(), - attempts: 0, - ownerId: '', - status: TaskStatus.Running, - startedAt: new Date(), - scheduledAt: new Date(), - retryAt: new Date(), - params: {}, - state: {}, - taskType: ManifestTaskConstants.TYPE, - }; const createTaskRunner = mockTaskManager.registerTaskDefinitions.mock.calls[0][0][ManifestTaskConstants.TYPE] .createTaskRunner; - const taskRunner = createTaskRunner({ taskInstance: mockTaskInstance }); + const taskRunner = createTaskRunner({ taskInstance: MOCK_TASK_INSTANCE }); await taskRunner.run(); expect(mockManifestTask.runTask).toHaveBeenCalled(); }); }); + + describe('Artifacts generation flow tests', () => { + const runTask = async (manifestManager: ManifestManager) => { + const mockContext = createMockEndpointAppContext(); + const mockTaskManager = taskManagerMock.createSetup(); + + new ManifestTask({ + endpointAppContext: mockContext, + taskManager: mockTaskManager, + }); + + mockContext.service.getManifestManager = jest.fn().mockReturnValue(manifestManager); + + const createTaskRunner = + mockTaskManager.registerTaskDefinitions.mock.calls[0][0][ManifestTaskConstants.TYPE] + .createTaskRunner; + const taskRunner = createTaskRunner({ taskInstance: MOCK_TASK_INSTANCE }); + await taskRunner.run(); + }; + + const TEST_POLICY_ID_1 = 'c6d16e42-c32d-4dce-8a88-113cfe276ad1'; + const TEST_POLICY_ID_2 = '93c46720-c217-11ea-9906-b5b8a21b268e'; + const ARTIFACT_ID_1 = + 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3'; + let ARTIFACT_EXCEPTIONS_MACOS: InternalArtifactCompleteSchema; + let ARTIFACT_EXCEPTIONS_WINDOWS: InternalArtifactCompleteSchema; + let ARTIFACT_TRUSTED_APPS_MACOS: InternalArtifactCompleteSchema; + + beforeAll(async () => { + const artifacts = await getMockArtifacts({ compress: true }); + ARTIFACT_EXCEPTIONS_MACOS = artifacts[0]; + ARTIFACT_EXCEPTIONS_WINDOWS = artifacts[1]; + ARTIFACT_TRUSTED_APPS_MACOS = artifacts[2]; + }); + + test('Should not run the process when no current manifest manager', async () => { + const manifestManager = buildManifestManagerMock(); + + manifestManager.getLastComputedManifest = jest.fn().mockReturnValue(null); + + await runTask(manifestManager); + + expect(manifestManager.getLastComputedManifest).toHaveBeenCalled(); + expect(manifestManager.buildNewManifest).not.toHaveBeenCalled(); + expect(manifestManager.pushArtifacts).not.toHaveBeenCalled(); + expect(manifestManager.commit).not.toHaveBeenCalled(); + expect(manifestManager.tryDispatch).not.toHaveBeenCalled(); + expect(manifestManager.deleteArtifacts).not.toHaveBeenCalled(); + }); + + test('Should stop the process when no building new manifest throws error', async () => { + const manifestManager = buildManifestManagerMock(); + const lastManifest = Manifest.getDefault(); + + manifestManager.getLastComputedManifest = jest.fn().mockReturnValue(lastManifest); + manifestManager.buildNewManifest = jest.fn().mockRejectedValue(new Error()); + + await runTask(manifestManager); + + expect(manifestManager.getLastComputedManifest).toHaveBeenCalled(); + expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(lastManifest); + expect(manifestManager.pushArtifacts).not.toHaveBeenCalled(); + expect(manifestManager.commit).not.toHaveBeenCalled(); + expect(manifestManager.tryDispatch).not.toHaveBeenCalled(); + expect(manifestManager.deleteArtifacts).not.toHaveBeenCalled(); + }); + + test('Should not bump version and commit manifest when no diff in the manifest', async () => { + const manifestManager = buildManifestManagerMock(); + + const lastManifest = Manifest.getDefault(); + lastManifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + lastManifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS); + + const newManifest = Manifest.getDefault(); + newManifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + newManifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS); + + manifestManager.getLastComputedManifest = jest.fn().mockReturnValue(lastManifest); + manifestManager.buildNewManifest = jest.fn().mockResolvedValue(newManifest); + manifestManager.pushArtifacts = jest.fn().mockResolvedValue([]); + manifestManager.tryDispatch = jest.fn().mockResolvedValue([]); + manifestManager.deleteArtifacts = jest.fn().mockResolvedValue([]); + + await runTask(manifestManager); + + expect(newManifest.getSemanticVersion()).toBe('1.0.0'); + + expect(manifestManager.getLastComputedManifest).toHaveBeenCalled(); + expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(lastManifest); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([]); + expect(manifestManager.commit).not.toHaveBeenCalled(); + expect(manifestManager.tryDispatch).toHaveBeenCalledWith(newManifest); + expect(manifestManager.deleteArtifacts).toHaveBeenCalledWith([]); + }); + + test('Should stop the process when there are errors pushing new artifacts', async () => { + const manifestManager = buildManifestManagerMock(); + + const lastManifest = Manifest.getDefault(); + + const newManifest = Manifest.getDefault(); + newManifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + newManifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS); + + manifestManager.getLastComputedManifest = jest.fn().mockReturnValue(lastManifest); + manifestManager.buildNewManifest = jest.fn().mockResolvedValue(newManifest); + manifestManager.pushArtifacts = jest.fn().mockResolvedValue([new Error()]); + + await runTask(manifestManager); + + expect(newManifest.getSemanticVersion()).toBe('1.0.0'); + + expect(manifestManager.getLastComputedManifest).toHaveBeenCalled(); + expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(lastManifest); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([ + ARTIFACT_EXCEPTIONS_MACOS, + ARTIFACT_TRUSTED_APPS_MACOS, + ]); + expect(manifestManager.commit).not.toHaveBeenCalled(); + expect(manifestManager.tryDispatch).not.toHaveBeenCalled(); + expect(manifestManager.deleteArtifacts).not.toHaveBeenCalled(); + }); + + test('Should stop the process when there are errors committing manifest', async () => { + const manifestManager = buildManifestManagerMock(); + + const lastManifest = Manifest.getDefault(); + + const newManifest = Manifest.getDefault(); + newManifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + newManifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS); + + manifestManager.getLastComputedManifest = jest.fn().mockReturnValue(lastManifest); + manifestManager.buildNewManifest = jest.fn().mockResolvedValue(newManifest); + manifestManager.pushArtifacts = jest.fn().mockResolvedValue([]); + manifestManager.commit = jest.fn().mockRejectedValue(new Error()); + + await runTask(manifestManager); + + expect(newManifest.getSemanticVersion()).toBe('1.0.1'); + + expect(manifestManager.getLastComputedManifest).toHaveBeenCalled(); + expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(lastManifest); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([ + ARTIFACT_EXCEPTIONS_MACOS, + ARTIFACT_TRUSTED_APPS_MACOS, + ]); + expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); + expect(manifestManager.tryDispatch).not.toHaveBeenCalled(); + expect(manifestManager.deleteArtifacts).not.toHaveBeenCalled(); + }); + + test('Should stop the process when there are errors dispatching manifest', async () => { + const manifestManager = buildManifestManagerMock(); + + const lastManifest = Manifest.getDefault(); + + const newManifest = Manifest.getDefault(); + newManifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + newManifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS); + + manifestManager.getLastComputedManifest = jest.fn().mockReturnValue(lastManifest); + manifestManager.buildNewManifest = jest.fn().mockResolvedValue(newManifest); + manifestManager.pushArtifacts = jest.fn().mockResolvedValue([]); + manifestManager.commit = jest.fn().mockResolvedValue(null); + manifestManager.tryDispatch = jest.fn().mockResolvedValue([new Error()]); + + await runTask(manifestManager); + + expect(newManifest.getSemanticVersion()).toBe('1.0.1'); + + expect(manifestManager.getLastComputedManifest).toHaveBeenCalled(); + expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(lastManifest); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([ + ARTIFACT_EXCEPTIONS_MACOS, + ARTIFACT_TRUSTED_APPS_MACOS, + ]); + expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); + expect(manifestManager.tryDispatch).toHaveBeenCalledWith(newManifest); + expect(manifestManager.deleteArtifacts).not.toHaveBeenCalled(); + }); + + test('Should succeed the process and delete old artifacts', async () => { + const manifestManager = buildManifestManagerMock(); + + const lastManifest = Manifest.getDefault(); + lastManifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + lastManifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS); + + const newManifest = Manifest.getDefault(); + newManifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + newManifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS); + + manifestManager.getLastComputedManifest = jest.fn().mockReturnValue(lastManifest); + manifestManager.buildNewManifest = jest.fn().mockResolvedValue(newManifest); + manifestManager.pushArtifacts = jest.fn().mockResolvedValue([]); + manifestManager.commit = jest.fn().mockResolvedValue(null); + manifestManager.tryDispatch = jest.fn().mockResolvedValue([]); + manifestManager.deleteArtifacts = jest.fn().mockResolvedValue([]); + + await runTask(manifestManager); + + expect(newManifest.getSemanticVersion()).toBe('1.0.1'); + + expect(manifestManager.getLastComputedManifest).toHaveBeenCalled(); + expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(lastManifest); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([ARTIFACT_TRUSTED_APPS_MACOS]); + expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); + expect(manifestManager.tryDispatch).toHaveBeenCalledWith(newManifest); + expect(manifestManager.deleteArtifacts).toHaveBeenCalledWith([ARTIFACT_ID_1]); + }); + + test('Should succeed the process but not add or delete artifacts when there are only transitions', async () => { + const manifestManager = buildManifestManagerMock(); + + const lastManifest = Manifest.getDefault(); + lastManifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + lastManifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + + const newManifest = Manifest.getDefault(); + newManifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + newManifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_2); + + manifestManager.getLastComputedManifest = jest.fn().mockReturnValue(lastManifest); + manifestManager.buildNewManifest = jest.fn().mockResolvedValue(newManifest); + manifestManager.pushArtifacts = jest.fn().mockResolvedValue([]); + manifestManager.commit = jest.fn().mockResolvedValue(null); + manifestManager.tryDispatch = jest.fn().mockResolvedValue([]); + manifestManager.deleteArtifacts = jest.fn().mockResolvedValue([]); + + await runTask(manifestManager); + + expect(newManifest.getSemanticVersion()).toBe('1.0.1'); + + expect(manifestManager.getLastComputedManifest).toHaveBeenCalled(); + expect(manifestManager.buildNewManifest).toHaveBeenCalledWith(lastManifest); + expect(manifestManager.pushArtifacts).toHaveBeenCalledWith([]); + expect(manifestManager.commit).toHaveBeenCalledWith(newManifest); + expect(manifestManager.tryDispatch).toHaveBeenCalledWith(newManifest); + expect(manifestManager.deleteArtifacts).toHaveBeenCalledWith([]); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts index 4f0d8671fb17..04dcb36bf4ea 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts @@ -12,8 +12,9 @@ import { TaskManagerStartContract, } from '../../../../../task_manager/server'; import { EndpointAppContext } from '../../types'; -import { reportErrors } from './common'; +import { getArtifactId, reportErrors } from './common'; import { InternalArtifactCompleteSchema } from '../../schemas/artifacts'; +import { isEmptyManifestDiff } from './manifest'; export const ManifestTaskConstants = { TIMEOUT: '1m', @@ -114,39 +115,23 @@ export class ManifestTask { return; } - // New computed manifest based on current state of exception list + // New computed manifest based on current manifest const newManifest = await manifestManager.buildNewManifest(oldManifest); - const diffs = newManifest.diff(oldManifest); - - // Compress new artifacts - const adds = diffs.filter((diff) => diff.type === 'add').map((diff) => diff.id); - for (const artifactId of adds) { - const compressError = await newManifest.compressArtifact(artifactId); - if (compressError) { - throw compressError; - } - } - // Persist new artifacts - const artifacts = adds - .map((artifactId) => newManifest.getArtifact(artifactId)) - .filter((artifact): artifact is InternalArtifactCompleteSchema => artifact !== undefined); - if (artifacts.length !== adds.length) { - throw new Error('Invalid artifact encountered.'); - } - const persistErrors = await manifestManager.pushArtifacts(artifacts); + const diff = newManifest.diff(oldManifest); + + const persistErrors = await manifestManager.pushArtifacts( + diff.additions as InternalArtifactCompleteSchema[] + ); if (persistErrors.length) { reportErrors(this.logger, persistErrors); throw new Error('Unable to persist new artifacts.'); } - // Commit latest manifest state, if different - if (diffs.length) { + if (!isEmptyManifestDiff(diff)) { + // Commit latest manifest state newManifest.bumpSemanticVersion(); - const error = await manifestManager.commit(newManifest); - if (error) { - throw error; - } + await manifestManager.commit(newManifest); } // Try dispatching to ingest-manager package policies @@ -157,8 +142,9 @@ export class ManifestTask { } // Try to clean up superceded artifacts - const deletes = diffs.filter((diff) => diff.type === 'delete').map((diff) => diff.id); - const deleteErrors = await manifestManager.deleteArtifacts(deletes); + const deleteErrors = await manifestManager.deleteArtifacts( + diff.removals.map((artifact) => getArtifactId(artifact)) + ); if (deleteErrors.length) { reportErrors(this.logger, deleteErrors); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.test.ts similarity index 98% rename from x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts rename to x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.test.ts index a2aff41b68df..32bd7379ade2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.test.ts @@ -25,7 +25,7 @@ import { loggingSystemMock, } from 'src/core/server/mocks'; import { ArtifactConstants } from '../../lib/artifacts'; -import { registerDownloadExceptionListRoute } from './download_exception_list'; +import { registerDownloadArtifactRoute } from './download_artifact'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { createMockEndpointAppContextServiceStartContract } from '../../mocks'; import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; @@ -121,7 +121,7 @@ describe('test alerts route', () => { ); endpointAppContextService.start(startContract); - registerDownloadExceptionListRoute( + registerDownloadArtifactRoute( routerMock, { logFactory: loggingSystemMock.create(), diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts similarity index 94% rename from x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts rename to x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts index 3dbaa137bb92..020b70ca0553 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts @@ -29,9 +29,9 @@ import { EndpointAppContext } from '../../types'; const allowlistBaseRoute: string = '/api/endpoint/artifacts'; /** - * Registers the exception list route to enable sensors to download an allowlist artifact + * Registers the artifact download route to enable sensors to download an allowlist artifact */ -export function registerDownloadExceptionListRoute( +export function registerDownloadArtifactRoute( router: IRouter, endpointContext: EndpointAppContext, cache: LRU @@ -49,7 +49,7 @@ export function registerDownloadExceptionListRoute( }, async (context, req, res) => { let scopedSOClient: SavedObjectsClientContract; - const logger = endpointContext.logFactory.get('download_exception_list'); + const logger = endpointContext.logFactory.get('download_artifact'); // The ApiKey must be associated with an enrolled Fleet agent try { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/index.ts index a279a9546d3e..a651f93cab09 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from './download_exception_list'; +export * from './download_artifact'; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts index 46d6e36d3deb..dedbcc25e237 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts @@ -46,14 +46,10 @@ export const getInternalArtifactMock = async ( export const getEmptyInternalArtifactMock = async ( os: string, schemaVersion: string, - opts?: { compress: boolean } + opts?: { compress: boolean }, + artifactName: string = ArtifactConstants.GLOBAL_ALLOWLIST_NAME ): Promise => { - const artifact = await buildArtifact( - { entries: [] }, - os, - schemaVersion, - ArtifactConstants.GLOBAL_ALLOWLIST_NAME - ); + const artifact = await buildArtifact({ entries: [] }, os, schemaVersion, artifactName); return opts?.compress ? compressArtifact(artifact) : artifact; }; @@ -74,7 +70,7 @@ export const getInternalArtifactMockWithDiffs = async ( }; export const getInternalManifestMock = (): InternalManifestSchema => ({ - ids: [], + artifacts: [], schemaVersion: 'v1', semanticVersion: '1.0.0', }); diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts index 47941c068657..675ed41e394a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts @@ -58,9 +58,17 @@ export const internalArtifactCreateSchema = t.intersection([ ]); export type InternalArtifactCreateSchema = t.TypeOf; +export const internalManifestEntrySchema = t.exact( + t.type({ + policyId: t.union([identifier, t.undefined]), + artifactId: identifier, + }) +); +export type InternalManifestEntrySchema = t.TypeOf; + export const internalManifestSchema = t.exact( t.type({ - ids: t.array(identifier), + artifacts: t.array(internalManifestEntrySchema), schemaVersion: manifestSchemaVersion, semanticVersion, }) diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.mock.ts deleted file mode 100644 index c16b10b965cc..000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.mock.ts +++ /dev/null @@ -1,19 +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 { savedObjectsClientMock } from 'src/core/server/mocks'; -import { SavedObjectsClientContract } from 'src/core/server'; -import { ArtifactClient } from './artifact_client'; - -export const getArtifactClientMock = ( - savedObjectsClient?: SavedObjectsClientContract -): ArtifactClient => { - if (savedObjectsClient !== undefined) { - return new ArtifactClient(savedObjectsClient); - } - return new ArtifactClient(savedObjectsClientMock.create()); -}; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts index a5fcd24dbc75..b3f098a96933 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts @@ -8,7 +8,6 @@ import { savedObjectsClientMock } from 'src/core/server/mocks'; import { ArtifactConstants, getArtifactId } from '../../lib/artifacts'; import { getInternalArtifactMock } from '../../schemas/artifacts/saved_objects.mock'; -import { getArtifactClientMock } from './artifact_client.mock'; import { ArtifactClient } from './artifact_client'; describe('artifact_client', () => { @@ -20,14 +19,14 @@ describe('artifact_client', () => { test('can get artifact', async () => { const savedObjectsClient = savedObjectsClientMock.create(); - const artifactClient = getArtifactClientMock(savedObjectsClient); + const artifactClient = new ArtifactClient(savedObjectsClient); await artifactClient.getArtifact('abcd'); expect(savedObjectsClient.get).toHaveBeenCalled(); }); test('can create artifact', async () => { const savedObjectsClient = savedObjectsClientMock.create(); - const artifactClient = getArtifactClientMock(savedObjectsClient); + const artifactClient = new ArtifactClient(savedObjectsClient); const artifact = await getInternalArtifactMock('linux', 'v1'); await artifactClient.createArtifact(artifact); expect(savedObjectsClient.create).toHaveBeenCalledWith( @@ -42,7 +41,7 @@ describe('artifact_client', () => { test('can delete artifact', async () => { const savedObjectsClient = savedObjectsClientMock.create(); - const artifactClient = getArtifactClientMock(savedObjectsClient); + const artifactClient = new ArtifactClient(savedObjectsClient); await artifactClient.deleteArtifact('abcd'); expect(savedObjectsClient.delete).toHaveBeenCalledWith( ArtifactConstants.SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index 53d3bdfcb656..a8bbfca0d41e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -5,16 +5,14 @@ * 2.0. */ +import LRU from 'lru-cache'; import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks'; import { Logger } from 'src/core/server'; import { PackagePolicyServiceInterface } from '../../../../../../fleet/server'; import { createPackagePolicyServiceMock } from '../../../../../../fleet/server/mocks'; import { ExceptionListClient } from '../../../../../../lists/server'; import { listMock } from '../../../../../../lists/server/mocks'; -import LRU from 'lru-cache'; -import { getArtifactClientMock } from '../artifact_client.mock'; -import { getManifestClientMock } from '../manifest_client.mock'; -import { ManifestManager } from './manifest_manager'; +import { ExceptionListItemSchema } from '../../../../../../lists/common/schemas/response'; import { createPackagePolicyWithManifestMock, createPackagePolicyWithInitialManifestMock, @@ -22,6 +20,32 @@ import { getMockArtifactsWithDiff, getEmptyMockArtifacts, } from '../../../lib/artifacts/mocks'; +import { ArtifactClient } from '../artifact_client'; +import { getManifestClientMock } from '../manifest_client.mock'; +import { ManifestManager, ManifestManagerContext } from './manifest_manager'; + +export const createExceptionListResponse = (data: ExceptionListItemSchema[], total?: number) => ({ + data, + page: 1, + per_page: 100, + total: total || data.length, +}); + +type FindExceptionListItemOptions = Parameters[0]; + +const FILTER_REGEXP = /^exception-list-agnostic\.attributes\.os_types:"(\w+)"$/; + +export const mockFindExceptionListItemResponses = ( + responses: Record> +) => { + return jest.fn().mockImplementation((options: FindExceptionListItemOptions) => { + const os = FILTER_REGEXP.test(options.filter || '') + ? options.filter!.match(FILTER_REGEXP)![1] + : ''; + + return createExceptionListResponse(responses[options.listId]?.[os] || []); + }); +}; export enum ManifestManagerMockType { InitialSystemState, @@ -29,28 +53,54 @@ export enum ManifestManagerMockType { NormalFlow, } -export const getManifestManagerMock = (opts?: { - mockType?: ManifestManagerMockType; - cache?: LRU; - exceptionListClient?: ExceptionListClient; - packagePolicyService?: jest.Mocked; - savedObjectsClient?: ReturnType; -}): ManifestManager => { - let cache = new LRU({ max: 10, maxAge: 1000 * 60 * 60 }); - if (opts?.cache != null) { - cache = opts.cache; - } +export interface ManifestManagerMockOptions { + cache: LRU; + exceptionListClient: ExceptionListClient; + packagePolicyService: jest.Mocked; + savedObjectsClient: ReturnType; +} - let exceptionListClient = listMock.getExceptionListClient(); - if (opts?.exceptionListClient != null) { - exceptionListClient = opts.exceptionListClient; - } +export const buildManifestManagerMockOptions = ( + opts: Partial +): ManifestManagerMockOptions => ({ + cache: new LRU({ max: 10, maxAge: 1000 * 60 * 60 }), + exceptionListClient: listMock.getExceptionListClient(), + packagePolicyService: createPackagePolicyServiceMock(), + savedObjectsClient: savedObjectsClientMock.create(), + ...opts, +}); - let packagePolicyService = createPackagePolicyServiceMock(); - if (opts?.packagePolicyService != null) { - packagePolicyService = opts.packagePolicyService; - } - packagePolicyService.list = jest.fn().mockResolvedValue({ +export const buildManifestManagerContextMock = ( + opts: Partial +): ManifestManagerContext => { + const fullOpts = buildManifestManagerMockOptions(opts); + + return { + ...fullOpts, + artifactClient: new ArtifactClient(fullOpts.savedObjectsClient), + logger: loggingSystemMock.create().get() as jest.Mocked, + }; +}; + +export const buildManifestManagerMock = (opts?: Partial) => { + const manifestManager = new ManifestManager(buildManifestManagerContextMock(opts || {})); + manifestManager.getLastComputedManifest = jest.fn(); + manifestManager.buildNewManifest = jest.fn(); + manifestManager.pushArtifacts = jest.fn(); + manifestManager.deleteArtifacts = jest.fn(); + manifestManager.commit = jest.fn(); + manifestManager.tryDispatch = jest.fn(); + + return manifestManager; +}; + +export const getManifestManagerMock = ( + opts?: Partial & { mockType?: ManifestManagerMockType } +): ManifestManager => { + const { mockType = ManifestManagerMockType.NormalFlow, ...restOptions } = opts || {}; + const context = buildManifestManagerContextMock(restOptions); + + context.packagePolicyService.list = jest.fn().mockResolvedValue({ total: 1, items: [ { version: 'policy-1-version', ...createPackagePolicyWithManifestMock() }, @@ -59,19 +109,13 @@ export const getManifestManagerMock = (opts?: { ], }); - let savedObjectsClient = savedObjectsClientMock.create(); - if (opts?.savedObjectsClient != null) { - savedObjectsClient = opts.savedObjectsClient; - } - class ManifestManagerMock extends ManifestManager { protected buildExceptionListArtifacts = jest.fn().mockImplementation(() => { - const mockType = opts?.mockType ?? ManifestManagerMockType.NormalFlow; switch (mockType) { case ManifestManagerMockType.InitialSystemState: return getEmptyMockArtifacts(); case ManifestManagerMockType.ListClientPromiseRejection: - exceptionListClient.findExceptionListItem = jest + context.exceptionListClient.findExceptionListItem = jest .fn() .mockRejectedValue(new Error('unexpected thing happened')); return super.buildExceptionListArtifacts('v1'); @@ -81,7 +125,6 @@ export const getManifestManagerMock = (opts?: { }); public getLastComputedManifest = jest.fn().mockImplementation(() => { - const mockType = opts?.mockType ?? ManifestManagerMockType.NormalFlow; switch (mockType) { case ManifestManagerMockType.InitialSystemState: return null; @@ -95,14 +138,5 @@ export const getManifestManagerMock = (opts?: { .mockReturnValue(getManifestClientMock(this.savedObjectsClient)); } - const manifestManager = new ManifestManagerMock({ - artifactClient: getArtifactClientMock(savedObjectsClient), - cache, - packagePolicyService, - exceptionListClient, - logger: loggingSystemMock.create().get() as jest.Mocked, - savedObjectsClient, - }); - - return manifestManager; + return new ManifestManagerMock(context); }; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index eedd3dad2cdb..52897f473189 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -6,326 +6,854 @@ */ import { inflateSync } from 'zlib'; +import { SavedObjectsErrorHelpers } from 'src/core/server'; import { savedObjectsClientMock } from 'src/core/server/mocks'; -import { createPackagePolicyServiceMock } from '../../../../../../fleet/server/mocks'; -import { ArtifactConstants, ManifestConstants, isCompleteArtifact } from '../../../lib/artifacts'; - -import { getManifestManagerMock, ManifestManagerMockType } from './manifest_manager.mock'; -import LRU from 'lru-cache'; - -describe('manifest_manager', () => { - describe('ManifestManager sanity checks', () => { - test('ManifestManager can retrieve and diff manifests', async () => { - const manifestManager = getManifestManagerMock(); - const oldManifest = await manifestManager.getLastComputedManifest(); - const newManifest = await manifestManager.buildNewManifest(oldManifest!); - expect(newManifest.diff(oldManifest!)).toEqual([ - { - id: - 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - type: 'delete', - }, - { - id: - 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - type: 'delete', - }, - { - id: - 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - type: 'delete', - }, - { - id: - 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - type: 'delete', - }, - { - id: - 'endpoint-exceptionlist-macos-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', - type: 'add', - }, - { - id: - 'endpoint-trustlist-macos-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - type: 'add', - }, - { - id: - 'endpoint-trustlist-windows-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - type: 'add', - }, - { - id: - 'endpoint-trustlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - type: 'add', - }, - ]); +import { ENDPOINT_LIST_ID } from '../../../../../../lists/common'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../../lists/common/constants'; +import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { PackagePolicy } from '../../../../../../fleet/common/types/models'; +import { getEmptyInternalArtifactMock } from '../../../schemas/artifacts/saved_objects.mock'; +import { + InternalArtifactCompleteSchema, + InternalArtifactSchema, + InternalManifestSchema, +} from '../../../schemas/artifacts'; +import { + createPackagePolicyWithConfigMock, + getMockArtifacts, + toArtifactRecords, +} from '../../../lib/artifacts/mocks'; +import { + ArtifactConstants, + ManifestConstants, + getArtifactId, + isCompressed, + translateToEndpointExceptions, + Manifest, +} from '../../../lib/artifacts'; + +import { + buildManifestManagerContextMock, + mockFindExceptionListItemResponses, +} from './manifest_manager.mock'; + +import { ManifestManager } from './manifest_manager'; + +const uncompressData = async (data: Buffer) => JSON.parse(await inflateSync(data).toString()); + +const uncompressArtifact = async (artifact: InternalArtifactSchema) => + uncompressData(Buffer.from(artifact.body!, 'base64')); + +describe('ManifestManager', () => { + const TEST_POLICY_ID_1 = 'c6d16e42-c32d-4dce-8a88-113cfe276ad1'; + const TEST_POLICY_ID_2 = '93c46720-c217-11ea-9906-b5b8a21b268e'; + const ARTIFACT_ID_EXCEPTIONS_MACOS = + 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3'; + const ARTIFACT_ID_EXCEPTIONS_WINDOWS = + 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3'; + const ARTIFACT_ID_TRUSTED_APPS_MACOS = + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3'; + const ARTIFACT_ID_TRUSTED_APPS_WINDOWS = + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3'; + + const ARTIFACT_NAME_EXCEPTIONS_MACOS = 'endpoint-exceptionlist-macos-v1'; + const ARTIFACT_NAME_EXCEPTIONS_WINDOWS = 'endpoint-exceptionlist-windows-v1'; + const ARTIFACT_NAME_TRUSTED_APPS_MACOS = 'endpoint-trustlist-macos-v1'; + const ARTIFACT_NAME_TRUSTED_APPS_WINDOWS = 'endpoint-trustlist-windows-v1'; + const ARTIFACT_NAME_TRUSTED_APPS_LINUX = 'endpoint-trustlist-linux-v1'; + + let ARTIFACTS: InternalArtifactCompleteSchema[] = []; + let ARTIFACTS_BY_ID: { [K: string]: InternalArtifactCompleteSchema } = {}; + let ARTIFACT_EXCEPTIONS_MACOS: InternalArtifactCompleteSchema; + let ARTIFACT_EXCEPTIONS_WINDOWS: InternalArtifactCompleteSchema; + let ARTIFACT_TRUSTED_APPS_MACOS: InternalArtifactCompleteSchema; + let ARTIFACT_TRUSTED_APPS_WINDOWS: InternalArtifactCompleteSchema; + + beforeAll(async () => { + ARTIFACTS = await getMockArtifacts({ compress: true }); + ARTIFACTS_BY_ID = { + [ARTIFACT_ID_EXCEPTIONS_MACOS]: ARTIFACTS[0], + [ARTIFACT_ID_EXCEPTIONS_WINDOWS]: ARTIFACTS[1], + [ARTIFACT_ID_TRUSTED_APPS_MACOS]: ARTIFACTS[2], + [ARTIFACT_ID_TRUSTED_APPS_WINDOWS]: ARTIFACTS[3], + }; + ARTIFACT_EXCEPTIONS_MACOS = ARTIFACTS[0]; + ARTIFACT_EXCEPTIONS_WINDOWS = ARTIFACTS[1]; + ARTIFACT_TRUSTED_APPS_MACOS = ARTIFACTS[2]; + ARTIFACT_TRUSTED_APPS_WINDOWS = ARTIFACTS[3]; + }); + + describe('getLastComputedManifest', () => { + test('Returns null when saved object not found', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const manifestManager = new ManifestManager( + buildManifestManagerContextMock({ savedObjectsClient }) + ); + + savedObjectsClient.get = jest.fn().mockRejectedValue({ output: { statusCode: 404 } }); + + expect(await manifestManager.getLastComputedManifest()).toBe(null); }); - test('ManifestManager populates cache properly', async () => { - const cache = new LRU({ max: 10, maxAge: 1000 * 60 * 60 }); - const manifestManager = getManifestManagerMock({ cache }); - const oldManifest = await manifestManager.getLastComputedManifest(); - const newManifest = await manifestManager.buildNewManifest(oldManifest!); - const diffs = newManifest.diff(oldManifest!); - expect(diffs).toEqual([ - { - id: - 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - type: 'delete', - }, - { - id: - 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - type: 'delete', - }, - { - id: - 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - type: 'delete', - }, - { - id: - 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - type: 'delete', - }, - { - id: - 'endpoint-exceptionlist-macos-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', - type: 'add', - }, - { - id: - 'endpoint-trustlist-macos-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - type: 'add', - }, - { - id: - 'endpoint-trustlist-windows-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - type: 'add', - }, - { - id: - 'endpoint-trustlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - type: 'add', - }, - ]); + test('Throws error when saved object client responds with 500', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const manifestManager = new ManifestManager( + buildManifestManagerContextMock({ savedObjectsClient }) + ); + const error = { output: { statusCode: 500 } }; - const firstNewArtifactId = diffs.find((diff) => diff.type === 'add')!.id; + savedObjectsClient.get = jest.fn().mockRejectedValue(error); - // Compress all `add` artifacts - for (const artifactDiff of diffs) { - if (artifactDiff.type === 'add') { - await newManifest.compressArtifact(artifactDiff.id); - } - } + await expect(manifestManager.getLastComputedManifest()).rejects.toStrictEqual(error); + }); + + test('Throws error when no version on the manifest', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const manifestManager = new ManifestManager( + buildManifestManagerContextMock({ savedObjectsClient }) + ); - const artifact = newManifest.getArtifact(firstNewArtifactId)!; + savedObjectsClient.get = jest.fn().mockResolvedValue({}); - if (isCompleteArtifact(artifact)) { - await manifestManager.pushArtifacts([artifact]); // caches the artifact - } else { - throw new Error('Artifact is missing a body.'); - } + await expect(manifestManager.getLastComputedManifest()).rejects.toStrictEqual( + new Error('No version returned for manifest.') + ); + }); + + test('Retrieves empty manifest successfully', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const manifestManager = new ManifestManager( + buildManifestManagerContextMock({ savedObjectsClient }) + ); + + savedObjectsClient.get = jest.fn().mockResolvedValue({ + attributes: { + created: '20-01-2020 10:00:00.000Z', + schemaVersion: 'v2', + semanticVersion: '1.0.0', + artifacts: [], + }, + version: '2.0.0', + }); + + const manifest = await manifestManager.getLastComputedManifest(); + + expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); + expect(manifest?.getSemanticVersion()).toStrictEqual('1.0.0'); + expect(manifest?.getSavedObjectVersion()).toStrictEqual('2.0.0'); + expect(manifest?.getAllArtifacts()).toStrictEqual([]); + }); + + test('Retrieves non empty manifest successfully', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const manifestManager = new ManifestManager( + buildManifestManagerContextMock({ savedObjectsClient }) + ); - const entry = JSON.parse(inflateSync(cache.get(firstNewArtifactId)! as Buffer).toString()); - expect(entry).toEqual({ - entries: [ - { - type: 'simple', - entries: [ - { - entries: [ - { - field: 'some.nested.field', - operator: 'included', - type: 'exact_cased', - value: 'some value', - }, + savedObjectsClient.get = jest + .fn() + .mockImplementation(async (objectType: string, id: string) => { + if (objectType === ManifestConstants.SAVED_OBJECT_TYPE) { + return { + attributes: { + created: '20-01-2020 10:00:00.000Z', + schemaVersion: 'v2', + semanticVersion: '1.0.0', + artifacts: [ + { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_WINDOWS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_WINDOWS, policyId: TEST_POLICY_ID_2 }, ], - field: 'some.parentField', - type: 'nested', - }, - { - field: 'some.not.nested.field', - operator: 'included', - type: 'exact_cased', - value: 'some value', }, - ], - }, - ], - }); + version: '2.0.0', + }; + } else if (objectType === ArtifactConstants.SAVED_OBJECT_TYPE) { + return { attributes: ARTIFACTS_BY_ID[id], version: '2.1.1' }; + } else { + return null; + } + }); + + const manifest = await manifestManager.getLastComputedManifest(); + + expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); + expect(manifest?.getSemanticVersion()).toStrictEqual('1.0.0'); + expect(manifest?.getSavedObjectVersion()).toStrictEqual('2.0.0'); + expect(manifest?.getAllArtifacts()).toStrictEqual(ARTIFACTS.slice(0, 4)); + expect(manifest?.isDefaultArtifact(ARTIFACT_EXCEPTIONS_MACOS)).toBe(true); + expect(manifest?.getArtifactTargetPolicies(ARTIFACT_EXCEPTIONS_MACOS)).toStrictEqual( + new Set() + ); + expect(manifest?.isDefaultArtifact(ARTIFACT_EXCEPTIONS_WINDOWS)).toBe(true); + expect(manifest?.getArtifactTargetPolicies(ARTIFACT_EXCEPTIONS_WINDOWS)).toStrictEqual( + new Set([TEST_POLICY_ID_1]) + ); + expect(manifest?.isDefaultArtifact(ARTIFACT_TRUSTED_APPS_MACOS)).toBe(false); + expect(manifest?.getArtifactTargetPolicies(ARTIFACT_TRUSTED_APPS_MACOS)).toStrictEqual( + new Set([TEST_POLICY_ID_1]) + ); + expect(manifest?.isDefaultArtifact(ARTIFACT_TRUSTED_APPS_WINDOWS)).toBe(false); + expect(manifest?.getArtifactTargetPolicies(ARTIFACT_TRUSTED_APPS_WINDOWS)).toStrictEqual( + new Set([TEST_POLICY_ID_1, TEST_POLICY_ID_2]) + ); }); + }); + + describe('buildNewManifest', () => { + const SUPPORTED_ARTIFACT_NAMES = [ + ARTIFACT_NAME_EXCEPTIONS_MACOS, + ARTIFACT_NAME_EXCEPTIONS_WINDOWS, + ARTIFACT_NAME_TRUSTED_APPS_MACOS, + ARTIFACT_NAME_TRUSTED_APPS_WINDOWS, + ARTIFACT_NAME_TRUSTED_APPS_LINUX, + ]; + + const getArtifactIds = (artifacts: InternalArtifactSchema[]) => + artifacts.map((artifact) => artifact.identifier); - test('ManifestManager cannot dispatch incomplete (uncompressed) artifact', async () => { - const packagePolicyService = createPackagePolicyServiceMock(); - const manifestManager = getManifestManagerMock({ packagePolicyService }); - const oldManifest = await manifestManager.getLastComputedManifest(); - const newManifest = await manifestManager.buildNewManifest(oldManifest!); - const dispatchErrors = await manifestManager.tryDispatch(newManifest); - expect(dispatchErrors.length).toEqual(1); - expect(dispatchErrors[0].message).toEqual('Invalid manifest'); + test('Fails when exception list list client fails', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + + context.exceptionListClient.findExceptionListItem = jest.fn().mockRejectedValue(new Error()); + + await expect(manifestManager.buildNewManifest()).rejects.toThrow(); }); - test('ManifestManager can dispatch manifest', async () => { - const packagePolicyService = createPackagePolicyServiceMock(); - const manifestManager = getManifestManagerMock({ packagePolicyService }); - const oldManifest = await manifestManager.getLastComputedManifest(); - const newManifest = await manifestManager.buildNewManifest(oldManifest!); - const diffs = newManifest.diff(oldManifest!); + test('Builds fully new manifest if no baseline parameter passed and no exception list items', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); - for (const artifactDiff of diffs) { - if (artifactDiff.type === 'add') { - await newManifest.compressArtifact(artifactDiff.id); - } - } + context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({}); - newManifest.bumpSemanticVersion(); + const manifest = await manifestManager.buildNewManifest(); - const dispatchErrors = await manifestManager.tryDispatch(newManifest); + expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); + expect(manifest?.getSemanticVersion()).toStrictEqual('1.0.0'); + expect(manifest?.getSavedObjectVersion()).toBeUndefined(); - expect(dispatchErrors).toEqual([]); + const artifacts = manifest.getAllArtifacts(); - // 2 policies updated... 1 is already up-to-date - expect(packagePolicyService.update.mock.calls.length).toEqual(2); + expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); + expect(artifacts.every(isCompressed)).toBe(true); - expect( - packagePolicyService.update.mock.calls[0][3].inputs[0].config!.artifact_manifest.value - ).toEqual({ - manifest_version: '1.0.1', - schema_version: 'v1', - artifacts: { - 'endpoint-exceptionlist-macos-v1': { - compression_algorithm: 'zlib', - encryption_algorithm: 'none', - decoded_sha256: '0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', - encoded_sha256: '57941169bb2c5416f9bd7224776c8462cb9a2be0fe8b87e6213e77a1d29be824', - decoded_size: 292, - encoded_size: 131, - relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-v1/0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', - }, - 'endpoint-exceptionlist-windows-v1': { - compression_algorithm: 'zlib', - encryption_algorithm: 'none', - decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', - decoded_size: 432, - encoded_size: 147, - relative_url: - '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', - }, - 'endpoint-trustlist-linux-v1': { - compression_algorithm: 'zlib', - decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - decoded_size: 287, - encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', - encoded_size: 133, - encryption_algorithm: 'none', - relative_url: - '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - }, - 'endpoint-trustlist-macos-v1': { - compression_algorithm: 'zlib', - decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - decoded_size: 287, - encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', - encoded_size: 133, - encryption_algorithm: 'none', - relative_url: - '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - }, - 'endpoint-trustlist-windows-v1': { - compression_algorithm: 'zlib', - decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - decoded_size: 287, - encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', - encoded_size: 133, - encryption_algorithm: 'none', - relative_url: - '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', - }, - }, - }); + for (const artifact of artifacts) { + expect(await uncompressArtifact(artifact)).toStrictEqual({ entries: [] }); + } }); - test('ManifestManager fails to dispatch on conflict', async () => { - const packagePolicyService = createPackagePolicyServiceMock(); - const manifestManager = getManifestManagerMock({ packagePolicyService }); - const oldManifest = await manifestManager.getLastComputedManifest(); - const newManifest = await manifestManager.buildNewManifest(oldManifest!); - const diffs = newManifest.diff(oldManifest!); + test('Builds fully new manifest if no baseline parameter passed and present exception list items', async () => { + const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); + const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + + context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ + [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, + [ENDPOINT_TRUSTED_APPS_LIST_ID]: { linux: [trustedAppListItem] }, + }); + + const manifest = await manifestManager.buildNewManifest(); + + expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); + expect(manifest?.getSemanticVersion()).toStrictEqual('1.0.0'); + expect(manifest?.getSavedObjectVersion()).toBeUndefined(); + + const artifacts = manifest.getAllArtifacts(); - for (const artifactDiff of diffs) { - if (artifactDiff.type === 'add') { - await newManifest.compressArtifact(artifactDiff.id); + expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); + expect(artifacts.every(isCompressed)).toBe(true); + + for (const artifact of artifacts) { + if (artifact.identifier === ARTIFACT_NAME_EXCEPTIONS_MACOS) { + expect(await uncompressArtifact(artifact)).toStrictEqual({ + entries: translateToEndpointExceptions([exceptionListItem], 'v1'), + }); + } else if (artifact.identifier === 'endpoint-trustlist-linux-v1') { + expect(await uncompressArtifact(artifact)).toStrictEqual({ + entries: translateToEndpointExceptions([trustedAppListItem], 'v1'), + }); + } else { + expect(await uncompressArtifact(artifact)).toStrictEqual({ entries: [] }); } } + }); - newManifest.bumpSemanticVersion(); + test('Reuses artifacts when baseline parameter passed and present exception list items', async () => { + const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); + const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); - packagePolicyService.update.mockRejectedValueOnce({ status: 409 }); - const dispatchErrors = await manifestManager.tryDispatch(newManifest); - expect(dispatchErrors).toEqual([{ status: 409 }]); - }); + context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ + [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, + }); - test('ManifestManager can commit manifest', async () => { - const savedObjectsClient: ReturnType< - typeof savedObjectsClientMock.create - > = savedObjectsClientMock.create(); - const manifestManager = getManifestManagerMock({ - savedObjectsClient, + const oldManifest = await manifestManager.buildNewManifest(); + + context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ + [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, + [ENDPOINT_TRUSTED_APPS_LIST_ID]: { linux: [trustedAppListItem] }, }); - const oldManifest = await manifestManager.getLastComputedManifest(); - const newManifest = await manifestManager.buildNewManifest(oldManifest!); - const diffs = newManifest.diff(oldManifest!); - const firstOldArtifactId = diffs.find((diff) => diff.type === 'delete')!.id; - const FirstNewArtifactId = diffs.find((diff) => diff.type === 'add')!.id; + const manifest = await manifestManager.buildNewManifest(oldManifest); + + expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); + expect(manifest?.getSemanticVersion()).toStrictEqual('1.0.0'); + expect(manifest?.getSavedObjectVersion()).toBeUndefined(); + + const artifacts = manifest.getAllArtifacts(); - // Compress all new artifacts - for (const artifactDiff of diffs) { - if (artifactDiff.type === 'add') { - await newManifest.compressArtifact(artifactDiff.id); + expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); + expect(artifacts.every(isCompressed)).toBe(true); + + for (const artifact of artifacts) { + if (artifact.identifier === ARTIFACT_NAME_EXCEPTIONS_MACOS) { + expect(artifact).toStrictEqual(oldManifest.getAllArtifacts()[0]); + } else if (artifact.identifier === 'endpoint-trustlist-linux-v1') { + expect(await uncompressArtifact(artifact)).toStrictEqual({ + entries: translateToEndpointExceptions([trustedAppListItem], 'v1'), + }); + } else { + expect(await uncompressArtifact(artifact)).toStrictEqual({ entries: [] }); } } + }); + }); - const artifact = newManifest.getArtifact(FirstNewArtifactId)!; - if (isCompleteArtifact(artifact)) { - await manifestManager.pushArtifacts([artifact]); - } else { - throw new Error('Artifact is missing a body.'); - } + describe('deleteArtifacts', () => { + test('Successfully invokes saved objects client', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + + context.savedObjectsClient.delete = jest.fn().mockResolvedValue({}); + + await expect( + manifestManager.deleteArtifacts([ + ARTIFACT_ID_EXCEPTIONS_MACOS, + ARTIFACT_ID_EXCEPTIONS_WINDOWS, + ]) + ).resolves.toStrictEqual([]); + + expect(context.savedObjectsClient.delete).toHaveBeenNthCalledWith( + 1, + ArtifactConstants.SAVED_OBJECT_TYPE, + ARTIFACT_ID_EXCEPTIONS_MACOS + ); + expect(context.savedObjectsClient.delete).toHaveBeenNthCalledWith( + 2, + ArtifactConstants.SAVED_OBJECT_TYPE, + ARTIFACT_ID_EXCEPTIONS_WINDOWS + ); + }); + + test('Returns errors for partial failures', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + const error = new Error(); + + context.savedObjectsClient.delete = jest + .fn() + .mockImplementation(async (type: string, id: string) => { + if (id === ARTIFACT_ID_EXCEPTIONS_WINDOWS) { + throw error; + } else { + return {}; + } + }); - await manifestManager.commit(newManifest); - await manifestManager.deleteArtifacts([firstOldArtifactId]); + await expect( + manifestManager.deleteArtifacts([ + ARTIFACT_ID_EXCEPTIONS_MACOS, + ARTIFACT_ID_EXCEPTIONS_WINDOWS, + ]) + ).resolves.toStrictEqual([error]); - // created new artifact - expect(savedObjectsClient.create.mock.calls[0][0]).toEqual( - ArtifactConstants.SAVED_OBJECT_TYPE + expect(context.savedObjectsClient.delete).toHaveBeenCalledTimes(2); + expect(context.savedObjectsClient.delete).toHaveBeenNthCalledWith( + 1, + ArtifactConstants.SAVED_OBJECT_TYPE, + ARTIFACT_ID_EXCEPTIONS_MACOS + ); + expect(context.savedObjectsClient.delete).toHaveBeenNthCalledWith( + 2, + ArtifactConstants.SAVED_OBJECT_TYPE, + ARTIFACT_ID_EXCEPTIONS_WINDOWS ); + }); + }); + + describe('pushArtifacts', () => { + test('Successfully invokes saved objects client and stores in the cache', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + + context.savedObjectsClient.create = jest + .fn() + .mockImplementation((type: string, artifact: InternalArtifactCompleteSchema) => artifact); + + await expect( + manifestManager.pushArtifacts([ARTIFACT_EXCEPTIONS_MACOS, ARTIFACT_EXCEPTIONS_WINDOWS]) + ).resolves.toStrictEqual([]); + + expect(context.savedObjectsClient.create).toHaveBeenCalledTimes(2); + expect(context.savedObjectsClient.create).toHaveBeenNthCalledWith( + 1, + ArtifactConstants.SAVED_OBJECT_TYPE, + { ...ARTIFACT_EXCEPTIONS_MACOS, created: expect.anything() }, + { id: ARTIFACT_ID_EXCEPTIONS_MACOS } + ); + expect(context.savedObjectsClient.create).toHaveBeenNthCalledWith( + 2, + ArtifactConstants.SAVED_OBJECT_TYPE, + { ...ARTIFACT_EXCEPTIONS_WINDOWS, created: expect.anything() }, + { id: ARTIFACT_ID_EXCEPTIONS_WINDOWS } + ); + expect( + await uncompressData(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_MACOS))!) + ).toStrictEqual(await uncompressArtifact(ARTIFACT_EXCEPTIONS_MACOS)); + expect( + await uncompressData(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_WINDOWS))!) + ).toStrictEqual(await uncompressArtifact(ARTIFACT_EXCEPTIONS_WINDOWS)); + }); + + test('Returns errors for partial failures', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + const error = new Error(); + const { body, ...incompleteArtifact } = ARTIFACT_TRUSTED_APPS_MACOS; + + context.savedObjectsClient.create = jest + .fn() + .mockImplementation(async (type: string, artifact: InternalArtifactCompleteSchema) => { + if (getArtifactId(artifact) === ARTIFACT_ID_EXCEPTIONS_WINDOWS) { + throw error; + } else { + return artifact; + } + }); + + await expect( + manifestManager.pushArtifacts([ + ARTIFACT_EXCEPTIONS_MACOS, + ARTIFACT_EXCEPTIONS_WINDOWS, + incompleteArtifact as InternalArtifactCompleteSchema, + ]) + ).resolves.toStrictEqual([ + error, + new Error(`Incomplete artifact: ${ARTIFACT_ID_TRUSTED_APPS_MACOS}`), + ]); - // committed new manifest - expect(savedObjectsClient.create.mock.calls[1][0]).toEqual( - ManifestConstants.SAVED_OBJECT_TYPE + expect(context.savedObjectsClient.create).toHaveBeenCalledTimes(2); + expect(context.savedObjectsClient.create).toHaveBeenNthCalledWith( + 1, + ArtifactConstants.SAVED_OBJECT_TYPE, + { ...ARTIFACT_EXCEPTIONS_MACOS, created: expect.anything() }, + { id: ARTIFACT_ID_EXCEPTIONS_MACOS } ); + expect( + await uncompressData(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_MACOS))!) + ).toStrictEqual(await uncompressArtifact(ARTIFACT_EXCEPTIONS_MACOS)); + expect(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_WINDOWS))).toBeUndefined(); + }); - // deleted old artifact - expect(savedObjectsClient.delete).toHaveBeenCalledWith( + test('Tolerates saved objects client conflict', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + + context.savedObjectsClient.create = jest + .fn() + .mockRejectedValue( + SavedObjectsErrorHelpers.createConflictError( + ArtifactConstants.SAVED_OBJECT_TYPE, + ARTIFACT_ID_EXCEPTIONS_MACOS + ) + ); + + await expect( + manifestManager.pushArtifacts([ARTIFACT_EXCEPTIONS_MACOS]) + ).resolves.toStrictEqual([]); + + expect(context.savedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(context.savedObjectsClient.create).toHaveBeenNthCalledWith( + 1, ArtifactConstants.SAVED_OBJECT_TYPE, - firstOldArtifactId + { ...ARTIFACT_EXCEPTIONS_MACOS, created: expect.anything() }, + { id: ARTIFACT_ID_EXCEPTIONS_MACOS } ); + expect(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_MACOS))).toBeUndefined(); }); + }); + + describe('commit', () => { + test('Creates new saved object if no saved object version', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + const manifest = Manifest.getDefault(); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_2); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + + context.savedObjectsClient.create = jest + .fn() + .mockImplementation((type: string, object: InternalManifestSchema) => object); - test('ManifestManager handles promise rejections when building artifacts', async () => { - // This test won't fail on an unhandled promise rejection, but it will cause - // an UnhandledPromiseRejectionWarning to be printed. - const manifestManager = getManifestManagerMock({ - mockType: ManifestManagerMockType.ListClientPromiseRejection, + await expect(manifestManager.commit(manifest)).resolves.toBeUndefined(); + + expect(context.savedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(context.savedObjectsClient.create).toHaveBeenNthCalledWith( + 1, + ManifestConstants.SAVED_OBJECT_TYPE, + { + artifacts: [ + { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: TEST_POLICY_ID_2 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_2 }, + ], + schemaVersion: 'v1', + semanticVersion: '1.0.0', + created: expect.anything(), + }, + { id: 'endpoint-manifest-v1' } + ); + }); + + test('Updates existing saved object if has saved object version', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + const manifest = new Manifest({ soVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_2); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + + context.savedObjectsClient.update = jest + .fn() + .mockImplementation((type: string, id: string, object: InternalManifestSchema) => object); + + await expect(manifestManager.commit(manifest)).resolves.toBeUndefined(); + + expect(context.savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(context.savedObjectsClient.update).toHaveBeenNthCalledWith( + 1, + ManifestConstants.SAVED_OBJECT_TYPE, + 'endpoint-manifest-v1', + { + artifacts: [ + { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: TEST_POLICY_ID_2 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_2 }, + ], + schemaVersion: 'v1', + semanticVersion: '1.0.0', + }, + { version: '1.0.0' } + ); + }); + + test('Throws error when saved objects client fails', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + const manifest = new Manifest({ soVersion: '1.0.0' }); + const error = new Error(); + + context.savedObjectsClient.update = jest.fn().mockRejectedValue(error); + + await expect(manifestManager.commit(manifest)).rejects.toBe(error); + + expect(context.savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(context.savedObjectsClient.update).toHaveBeenNthCalledWith( + 1, + ManifestConstants.SAVED_OBJECT_TYPE, + 'endpoint-manifest-v1', + { + artifacts: [], + schemaVersion: 'v1', + semanticVersion: '1.0.0', + }, + { version: '1.0.0' } + ); + }); + }); + + describe('tryDispatch', () => { + const mockPolicyListResponse = (items: PackagePolicy[]) => + jest.fn().mockResolvedValue({ + items, + page: 1, + per_page: 100, + total: items.length, }); - await expect(manifestManager.buildNewManifest()).rejects.toThrow(); + + const toNewPackagePolicy = (packagePolicy: PackagePolicy) => { + const { id, revision, updated_at: updatedAt, updated_by: updatedBy, ...rest } = packagePolicy; + + return rest; + }; + + test('Should not dispatch if no policies', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + const manifest = new Manifest({ soVersion: '1.0.0' }); + + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + + context.packagePolicyService.list = mockPolicyListResponse([]); + + await expect(manifestManager.tryDispatch(manifest)).resolves.toStrictEqual([]); + + expect(context.packagePolicyService.update).toHaveBeenCalledTimes(0); + }); + + test('Should return errors if invalid config for package policy', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + + const manifest = new Manifest({ soVersion: '1.0.0' }); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + + context.packagePolicyService.list = mockPolicyListResponse([ + createPackagePolicyWithConfigMock({ id: TEST_POLICY_ID_1 }), + ]); + + await expect(manifestManager.tryDispatch(manifest)).resolves.toStrictEqual([ + new Error(`Package Policy ${TEST_POLICY_ID_1} has no config.`), + ]); + + expect(context.packagePolicyService.update).toHaveBeenCalledTimes(0); + }); + + test('Should not dispatch if semantic version has not changed', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + + const manifest = new Manifest({ soVersion: '1.0.0' }); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + + context.packagePolicyService.list = mockPolicyListResponse([ + createPackagePolicyWithConfigMock({ + id: TEST_POLICY_ID_1, + config: { + artifact_manifest: { + value: { + artifacts: toArtifactRecords({ + [ARTIFACT_NAME_EXCEPTIONS_WINDOWS]: ARTIFACT_EXCEPTIONS_MACOS, + }), + manifest_version: '1.0.0', + schema_version: 'v1', + }, + }, + }, + }), + ]); + + await expect(manifestManager.tryDispatch(manifest)).resolves.toStrictEqual([]); + + expect(context.packagePolicyService.update).toHaveBeenCalledTimes(0); + }); + + test('Should dispatch to only policies where list of artifacts changed', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + + const manifest = new Manifest({ soVersion: '1.0.0', semanticVersion: '1.0.1' }); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_2); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + + context.packagePolicyService.list = mockPolicyListResponse([ + createPackagePolicyWithConfigMock({ + id: TEST_POLICY_ID_1, + config: { + artifact_manifest: { + value: { + artifacts: toArtifactRecords({ + [ARTIFACT_NAME_EXCEPTIONS_MACOS]: ARTIFACT_EXCEPTIONS_MACOS, + }), + manifest_version: '1.0.0', + schema_version: 'v1', + }, + }, + }, + }), + createPackagePolicyWithConfigMock({ + id: TEST_POLICY_ID_2, + config: { + artifact_manifest: { + value: { + artifacts: toArtifactRecords({ + [ARTIFACT_NAME_EXCEPTIONS_WINDOWS]: ARTIFACT_EXCEPTIONS_WINDOWS, + [ARTIFACT_NAME_TRUSTED_APPS_MACOS]: ARTIFACT_TRUSTED_APPS_MACOS, + }), + manifest_version: '1.0.0', + schema_version: 'v1', + }, + }, + }, + }), + ]); + context.packagePolicyService.update = jest.fn().mockResolvedValue({}); + + await expect(manifestManager.tryDispatch(manifest)).resolves.toStrictEqual([]); + + expect(context.packagePolicyService.update).toHaveBeenCalledTimes(1); + expect(context.packagePolicyService.update).toHaveBeenNthCalledWith( + 1, + expect.anything(), + undefined, + TEST_POLICY_ID_1, + toNewPackagePolicy( + createPackagePolicyWithConfigMock({ + id: TEST_POLICY_ID_1, + config: { + artifact_manifest: { + value: { + artifacts: toArtifactRecords({ + [ARTIFACT_NAME_EXCEPTIONS_WINDOWS]: ARTIFACT_EXCEPTIONS_WINDOWS, + }), + manifest_version: '1.0.1', + schema_version: 'v1', + }, + }, + }, + }) + ) + ); + }); + + test('Should dispatch to only policies where artifact content changed', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + + const manifest = new Manifest({ soVersion: '1.0.0', semanticVersion: '1.0.1' }); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS, TEST_POLICY_ID_1); + manifest.addEntry(ARTIFACT_EXCEPTIONS_WINDOWS, TEST_POLICY_ID_2); + manifest.addEntry(ARTIFACT_TRUSTED_APPS_MACOS, TEST_POLICY_ID_2); + + context.packagePolicyService.list = mockPolicyListResponse([ + createPackagePolicyWithConfigMock({ + id: TEST_POLICY_ID_1, + config: { + artifact_manifest: { + value: { + artifacts: toArtifactRecords({ + [ARTIFACT_NAME_EXCEPTIONS_MACOS]: await getEmptyInternalArtifactMock( + 'macos', + 'v1', + { + compress: true, + } + ), + }), + manifest_version: '1.0.0', + schema_version: 'v1', + }, + }, + }, + }), + createPackagePolicyWithConfigMock({ + id: TEST_POLICY_ID_2, + config: { + artifact_manifest: { + value: { + artifacts: toArtifactRecords({ + [ARTIFACT_NAME_EXCEPTIONS_WINDOWS]: ARTIFACT_EXCEPTIONS_WINDOWS, + [ARTIFACT_NAME_TRUSTED_APPS_MACOS]: ARTIFACT_TRUSTED_APPS_MACOS, + }), + manifest_version: '1.0.0', + schema_version: 'v1', + }, + }, + }, + }), + ]); + context.packagePolicyService.update = jest.fn().mockResolvedValue({}); + + await expect(manifestManager.tryDispatch(manifest)).resolves.toStrictEqual([]); + + expect(context.packagePolicyService.update).toHaveBeenCalledTimes(1); + expect(context.packagePolicyService.update).toHaveBeenNthCalledWith( + 1, + expect.anything(), + undefined, + TEST_POLICY_ID_1, + toNewPackagePolicy( + createPackagePolicyWithConfigMock({ + id: TEST_POLICY_ID_1, + config: { + artifact_manifest: { + value: { + artifacts: toArtifactRecords({ + [ARTIFACT_NAME_EXCEPTIONS_MACOS]: ARTIFACT_EXCEPTIONS_MACOS, + }), + manifest_version: '1.0.1', + schema_version: 'v1', + }, + }, + }, + }) + ) + ); + }); + + test('Should return partial errors', async () => { + const context = buildManifestManagerContextMock({}); + const manifestManager = new ManifestManager(context); + const error = new Error(); + + const manifest = new Manifest({ soVersion: '1.0.0', semanticVersion: '1.0.1' }); + manifest.addEntry(ARTIFACT_EXCEPTIONS_MACOS); + + context.packagePolicyService.list = mockPolicyListResponse([ + createPackagePolicyWithConfigMock({ + id: TEST_POLICY_ID_1, + config: { + artifact_manifest: { + value: { + artifacts: {}, + manifest_version: '1.0.0', + schema_version: 'v1', + }, + }, + }, + }), + createPackagePolicyWithConfigMock({ + id: TEST_POLICY_ID_2, + config: { + artifact_manifest: { + value: { + artifacts: {}, + manifest_version: '1.0.0', + schema_version: 'v1', + }, + }, + }, + }), + ]); + context.packagePolicyService.update = jest.fn().mockImplementation(async (...args) => { + if (args[2] === TEST_POLICY_ID_2) { + throw error; + } else { + return {}; + } + }); + + await expect(manifestManager.tryDispatch(manifest)).resolves.toStrictEqual([error]); + + expect(context.packagePolicyService.update).toHaveBeenCalledTimes(2); }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 8e4d2d9349bb..6b9cbb55415a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -6,20 +6,25 @@ */ import semver from 'semver'; -import { Logger, SavedObjectsClientContract } from 'src/core/server'; import LRU from 'lru-cache'; +import { isEqual } from 'lodash'; +import { Logger, SavedObjectsClientContract } from 'src/core/server'; import { PackagePolicyServiceInterface } from '../../../../../../fleet/server'; import { ExceptionListClient } from '../../../../../../lists/server'; import { ManifestSchemaVersion } from '../../../../../common/endpoint/schema/common'; -import { manifestDispatchSchema } from '../../../../../common/endpoint/schema/manifest'; +import { + manifestDispatchSchema, + ManifestSchema, +} from '../../../../../common/endpoint/schema/manifest'; import { ArtifactConstants, buildArtifact, getArtifactId, getFullEndpointExceptionList, + isCompressed, Manifest, - ManifestDiff, + maybeCompressArtifact, } from '../../../lib/artifacts'; import { InternalArtifactCompleteSchema, @@ -29,6 +34,7 @@ import { ArtifactClient } from '../artifact_client'; import { ManifestClient } from '../manifest_client'; import { ENDPOINT_LIST_ID } from '../../../../../../lists/common'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../../lists/common/constants'; +import { PackagePolicy } from '../../../../../../fleet/common/types/models'; export interface ManifestManagerContext { savedObjectsClient: SavedObjectsClientContract; @@ -39,14 +45,13 @@ export interface ManifestManagerContext { cache: LRU; } -export interface ManifestSnapshotOpts { - initialize?: boolean; -} +const getArtifactIds = (manifest: ManifestSchema) => + [...Object.keys(manifest.artifacts)].map( + (key) => `${key}-${manifest.artifacts[key].decoded_sha256}` + ); -export interface ManifestSnapshot { - manifest: Manifest; - diffs: ManifestDiff[]; -} +const manifestsEqual = (manifest1: ManifestSchema, manifest2: ManifestSchema) => + isEqual(new Set(getArtifactIds(manifest1)), new Set(getArtifactIds(manifest2))); export class ManifestManager { protected artifactClient: ArtifactClient; @@ -209,8 +214,7 @@ export class ManifestManager { */ public async getLastComputedManifest(): Promise { try { - const manifestClient = this.getManifestClient(); - const manifestSo = await manifestClient.getManifest(); + const manifestSo = await this.getManifestClient().getManifest(); if (manifestSo.version === undefined) { throw new Error('No version returned for manifest.'); @@ -222,14 +226,17 @@ export class ManifestManager { soVersion: manifestSo.version, }); - for (const id of manifestSo.attributes.ids) { - const artifactSo = await this.artifactClient.getArtifact(id); - manifest.addEntry(artifactSo.attributes); + for (const entry of manifestSo.attributes.artifacts) { + manifest.addEntry( + (await this.artifactClient.getArtifact(entry.artifactId)).attributes, + entry.policyId + ); } + return manifest; - } catch (err) { - if (err.output.statusCode !== 404) { - throw err; + } catch (error) { + if (!error.output || error.output.statusCode !== 404) { + throw error; } return null; } @@ -241,17 +248,36 @@ export class ManifestManager { * @param baselineManifest A baseline manifest to use for initializing pre-existing artifacts. * @returns {Promise} A new Manifest object reprenting the current exception list. */ - public async buildNewManifest(baselineManifest?: Manifest): Promise { + public async buildNewManifest( + baselineManifest: Manifest = Manifest.getDefault(this.schemaVersion) + ): Promise { // Build new exception list artifacts const artifacts = ( await Promise.all([this.buildExceptionListArtifacts(), this.buildTrustedAppsArtifacts()]) ).flat(); // Build new manifest - const manifest = Manifest.fromArtifacts( - artifacts, - baselineManifest ?? Manifest.getDefault(this.schemaVersion) - ); + const manifest = new Manifest({ + schemaVersion: this.schemaVersion, + semanticVersion: baselineManifest.getSemanticVersion(), + soVersion: baselineManifest.getSavedObjectVersion(), + }); + + for (const artifact of artifacts) { + let artifactToAdd = baselineManifest.getArtifact(getArtifactId(artifact)) || artifact; + + if (!isCompressed(artifactToAdd)) { + artifactToAdd = await maybeCompressArtifact(artifactToAdd); + + if (!isCompressed(artifactToAdd)) { + throw new Error(`Unable to compress artifact: ${getArtifactId(artifactToAdd)}`); + } else if (!internalArtifactCompleteSchema.is(artifactToAdd)) { + throw new Error(`Incomplete artifact detected: ${getArtifactId(artifactToAdd)}`); + } + } + + manifest.addEntry(artifactToAdd); + } return manifest; } @@ -264,35 +290,24 @@ export class ManifestManager { * @returns {Promise} Any errors encountered. */ public async tryDispatch(manifest: Manifest): Promise { - const serializedManifest = manifest.toEndpointFormat(); - if (!manifestDispatchSchema.is(serializedManifest)) { - return [new Error('Invalid manifest')]; - } - - let paging = true; - let page = 1; const errors: Error[] = []; - while (paging) { - const { items, total } = await this.packagePolicyService.list(this.savedObjectsClient, { - page, - perPage: 20, - kuery: 'ingest-package-policies.package.name:endpoint', - }); + await this.forEachPolicy(async (packagePolicy) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { id, revision, updated_at, updated_by, ...newPackagePolicy } = packagePolicy; + if (newPackagePolicy.inputs.length > 0 && newPackagePolicy.inputs[0].config !== undefined) { + const oldManifest = newPackagePolicy.inputs[0].config.artifact_manifest ?? { + value: {}, + }; - for (const packagePolicy of items) { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { id, revision, updated_at, updated_by, ...newPackagePolicy } = packagePolicy; - if (newPackagePolicy.inputs.length > 0 && newPackagePolicy.inputs[0].config !== undefined) { - const oldManifest = newPackagePolicy.inputs[0].config.artifact_manifest ?? { - value: {}, - }; - - const newManifestVersion = manifest.getSemanticVersion(); - if (semver.gt(newManifestVersion, oldManifest.value.manifest_version)) { - newPackagePolicy.inputs[0].config.artifact_manifest = { - value: serializedManifest, - }; + const newManifestVersion = manifest.getSemanticVersion(); + if (semver.gt(newManifestVersion, oldManifest.value.manifest_version)) { + const serializedManifest = manifest.toPackagePolicyManifest(packagePolicy.id); + + if (!manifestDispatchSchema.is(serializedManifest)) { + errors.push(new Error(`Invalid manifest for policy ${packagePolicy.id}`)); + } else if (!manifestsEqual(serializedManifest, oldManifest.value)) { + newPackagePolicy.inputs[0].config.artifact_manifest = { value: serializedManifest }; try { await this.packagePolicyService.update( @@ -309,15 +324,17 @@ export class ManifestManager { errors.push(err); } } else { - this.logger.debug(`No change in package policy: ${id}`); + this.logger.debug( + `No change in manifest content for package policy: ${id}. Staying on old version` + ); } } else { - errors.push(new Error(`Package Policy ${id} has no config.`)); + this.logger.debug(`No change in manifest version for package policy: ${id}`); } + } else { + errors.push(new Error(`Package Policy ${id} has no config.`)); } - paging = (page - 1) * 20 + items.length < total; - page++; - } + }); return errors; } @@ -328,27 +345,41 @@ export class ManifestManager { * @param manifest The Manifest to commit. * @returns {Promise} An error, if encountered, or null. */ - public async commit(manifest: Manifest): Promise { - try { - const manifestClient = this.getManifestClient(); + public async commit(manifest: Manifest) { + const manifestClient = this.getManifestClient(); + + // Commit the new manifest + const manifestSo = manifest.toSavedObject(); + const version = manifest.getSavedObjectVersion(); + + if (version == null) { + await manifestClient.createManifest(manifestSo); + } else { + await manifestClient.updateManifest(manifestSo, { + version, + }); + } - // Commit the new manifest - const manifestSo = manifest.toSavedObject(); - const version = manifest.getSavedObjectVersion(); + this.logger.info(`Committed manifest ${manifest.getSemanticVersion()}`); + } - if (version == null) { - await manifestClient.createManifest(manifestSo); - } else { - await manifestClient.updateManifest(manifestSo, { - version, - }); + private async forEachPolicy(callback: (policy: PackagePolicy) => Promise) { + let paging = true; + let page = 1; + + while (paging) { + const { items, total } = await this.packagePolicyService.list(this.savedObjectsClient, { + page, + perPage: 20, + kuery: 'ingest-package-policies.package.name:endpoint', + }); + + for (const packagePolicy of items) { + await callback(packagePolicy); } - this.logger.info(`Committed manifest ${manifest.getSemanticVersion()}`); - } catch (err) { - return err; + paging = (page - 1) * 20 + items.length < total; + page++; } - - return null; } } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index a34193937c78..6e03d81a7d35 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -62,7 +62,7 @@ import { registerPolicyRoutes } from './endpoint/routes/policy'; import { ArtifactClient, ManifestManager } from './endpoint/services'; import { EndpointAppContextService } from './endpoint/endpoint_app_context_services'; import { EndpointAppContext } from './endpoint/types'; -import { registerDownloadExceptionListRoute } from './endpoint/routes/artifacts'; +import { registerDownloadArtifactRoute } from './endpoint/routes/artifacts'; import { initUsageCollectors } from './usage'; import type { SecuritySolutionRequestHandlerContext } from './types'; import { registerTrustedAppsRoutes } from './endpoint/routes/trusted_apps'; @@ -130,7 +130,7 @@ export class Plugin implements IPlugin; + private artifactsCache: LRU; constructor(context: PluginInitializerContext) { this.context = context; @@ -138,7 +138,7 @@ export class Plugin implements IPlugin({ max: 3, maxAge: 1000 * 60 * 5 }); + this.artifactsCache = new LRU({ max: 3, maxAge: 1000 * 60 * 5 }); this.telemetryEventsSender = new TelemetryEventsSender(this.logger); this.logger.debug('plugin initialized'); @@ -192,7 +192,7 @@ export class Plugin implements IPlugin Date: Wed, 10 Feb 2021 10:52:46 -0500 Subject: [PATCH 09/32] [ML] Data Frame Analytics creation wizard: adds support for extended hyper-parameters (#90843) * add support for new hyperparameters in the creation wizard * fix translation error --- .../ml/common/util/errors/process_errors.ts | 12 +- x-pack/plugins/ml/common/util/errors/types.ts | 2 + .../data_frame_analytics/common/analytics.ts | 6 + .../advanced_step/advanced_step_form.tsx | 16 +- .../advanced_step/hyper_parameters.tsx | 224 +++++++++++++++++- .../components/shared/fetch_explain_data.ts | 10 +- .../action_clone/clone_action_name.tsx | 48 ++++ .../hooks/use_create_analytics_form/state.ts | 28 ++- 8 files changed, 340 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/ml/common/util/errors/process_errors.ts b/x-pack/plugins/ml/common/util/errors/process_errors.ts index dd97886f2ff3..821ba670e2dd 100644 --- a/x-pack/plugins/ml/common/util/errors/process_errors.ts +++ b/x-pack/plugins/ml/common/util/errors/process_errors.ts @@ -59,11 +59,21 @@ export const extractErrorProperties = (error: ErrorType): MLErrorObject => { typeof error.body.attributes === 'object' && typeof error.body.attributes.body?.error?.reason === 'string' ) { - return { + const errObj: MLErrorObject = { message: error.body.attributes.body.error.reason, statusCode: error.body.statusCode, fullError: error.body.attributes.body, }; + if ( + typeof error.body.attributes.body.error.caused_by === 'object' && + (typeof error.body.attributes.body.error.caused_by?.reason === 'string' || + typeof error.body.attributes.body.error.caused_by?.caused_by?.reason === 'string') + ) { + errObj.causedBy = + error.body.attributes.body.error.caused_by?.caused_by?.reason || + error.body.attributes.body.error.caused_by?.reason; + } + return errObj; } else { return { message: error.body.message, diff --git a/x-pack/plugins/ml/common/util/errors/types.ts b/x-pack/plugins/ml/common/util/errors/types.ts index 23cd91a57c4f..39e9ed4e2575 100644 --- a/x-pack/plugins/ml/common/util/errors/types.ts +++ b/x-pack/plugins/ml/common/util/errors/types.ts @@ -11,6 +11,7 @@ import Boom from '@hapi/boom'; export interface EsErrorRootCause { type: string; reason: string; + caused_by?: EsErrorRootCause; } export interface EsErrorBody { @@ -37,6 +38,7 @@ export interface ErrorMessage { } export interface MLErrorObject { + causedBy?: string; message: string; statusCode?: number; fullError?: EsErrorBody; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 929b055b5f7b..4f1799ed26f8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -33,18 +33,24 @@ export { getAnalysisType } from '../../../../common/util/analytics_utils'; export type IndexPattern = string; export enum ANALYSIS_ADVANCED_FIELDS { + ALPHA = 'alpha', ETA = 'eta', + ETA_GROWTH_RATE_PER_TREE = 'eta_growth_rate_per_tree', + DOWNSAMPLE_FACTOR = 'downsample_factor', FEATURE_BAG_FRACTION = 'feature_bag_fraction', FEATURE_INFLUENCE_THRESHOLD = 'feature_influence_threshold', GAMMA = 'gamma', LAMBDA = 'lambda', MAX_TREES = 'max_trees', + MAX_OPTIMIZATION_ROUNDS_PER_HYPERPARAMETER = 'max_optimization_rounds_per_hyperparameter', METHOD = 'method', N_NEIGHBORS = 'n_neighbors', NUM_TOP_CLASSES = 'num_top_classes', NUM_TOP_FEATURE_IMPORTANCE_VALUES = 'num_top_feature_importance_values', OUTLIER_FRACTION = 'outlier_fraction', RANDOMIZE_SEED = 'randomize_seed', + SOFT_TREE_DEPTH_LIMIT = 'soft_tree_depth_limit', + SOFT_TREE_DEPTH_TOLERANCE = 'soft_tree_depth_tolerance', } export enum OUTLIER_ANALYSIS_METHOD { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx index 67ade39d6fa7..8e25fc961c7c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx @@ -138,14 +138,18 @@ export const AdvancedStepForm: FC = ({ const { setEstimatedModelMemoryLimit, setFormState } = actions; const { form, isJobCreated, estimatedModelMemoryLimit } = state; const { + alpha, computeFeatureInfluence, + downsampleFactor, eta, + etaGrowthRatePerTree, featureBagFraction, featureInfluenceThreshold, gamma, jobType, lambda, maxNumThreads, + maxOptimizationRoundsPerHyperparameter, maxTrees, method, modelMemoryLimit, @@ -157,6 +161,8 @@ export const AdvancedStepForm: FC = ({ outlierFraction, predictionFieldName, randomizeSeed, + softTreeDepthLimit, + softTreeDepthTolerance, useEstimatedMml, } = form; @@ -197,7 +203,7 @@ export const AdvancedStepForm: FC = ({ useEffect(() => { setFetchingAdvancedParamErrors(true); (async function () { - const { success, errorMessage, expectedMemory } = await fetchExplainData(form); + const { success, errorMessage, errorReason, expectedMemory } = await fetchExplainData(form); const paramErrors: AdvancedParamErrors = {}; if (success) { @@ -212,6 +218,8 @@ export const AdvancedStepForm: FC = ({ Object.values(ANALYSIS_ADVANCED_FIELDS).forEach((param) => { if (errorMessage.includes(`[${param}]`)) { paramErrors[param] = errorMessage; + } else if (errorReason?.includes(`[${param}]`)) { + paramErrors[param] = errorReason; } }); } @@ -219,12 +227,16 @@ export const AdvancedStepForm: FC = ({ setAdvancedParamErrors(paramErrors); })(); }, [ + alpha, + downsampleFactor, eta, + etaGrowthRatePerTree, featureBagFraction, featureInfluenceThreshold, gamma, lambda, maxNumThreads, + maxOptimizationRoundsPerHyperparameter, maxTrees, method, nNeighbors, @@ -232,6 +244,8 @@ export const AdvancedStepForm: FC = ({ numTopFeatureImportanceValues, outlierFraction, randomizeSeed, + softTreeDepthLimit, + softTreeDepthTolerance, ]); const outlierDetectionAdvancedConfig = ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx index 0bd817f5e275..03dfc09d97b0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx @@ -21,7 +21,20 @@ interface Props extends CreateAnalyticsFormProps { export const HyperParameters: FC = ({ actions, state, advancedParamErrors }) => { const { setFormState } = actions; - const { eta, featureBagFraction, gamma, lambda, maxTrees, randomizeSeed } = state.form; + const { + alpha, + downsampleFactor, + eta, + etaGrowthRatePerTree, + featureBagFraction, + gamma, + lambda, + maxOptimizationRoundsPerHyperparameter, + maxTrees, + randomizeSeed, + softTreeDepthLimit, + softTreeDepthTolerance, + } = state.form; return ( @@ -203,6 +216,215 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors /> + + + + setFormState({ alpha: e.target.value === '' ? undefined : +e.target.value }) + } + step={0.001} + min={0} + value={getNumberValue(alpha)} + /> + + + + + + setFormState({ + downsampleFactor: e.target.value === '' ? undefined : +e.target.value, + }) + } + step={0.001} + min={0} + max={1} + value={getNumberValue(downsampleFactor)} + /> + + + + + + setFormState({ + etaGrowthRatePerTree: e.target.value === '' ? undefined : +e.target.value, + }) + } + step={0.001} + min={0.5} + max={2} + value={getNumberValue(etaGrowthRatePerTree)} + /> + + + + + + setFormState({ + maxOptimizationRoundsPerHyperparameter: + e.target.value === '' ? undefined : +e.target.value, + }) + } + min={0} + max={20} + step={1} + value={getNumberValue(maxOptimizationRoundsPerHyperparameter)} + /> + + + + + + setFormState({ + softTreeDepthLimit: e.target.value === '' ? undefined : +e.target.value, + }) + } + step={0.001} + min={0} + value={getNumberValue(softTreeDepthLimit)} + /> + + + + + + setFormState({ + softTreeDepthTolerance: e.target.value === '' ? undefined : +e.target.value, + }) + } + step={0.001} + min={0.01} + value={getNumberValue(softTreeDepthTolerance)} + /> + + ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts index 9b71b5d29c0f..ec567f1f9615 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts @@ -6,7 +6,7 @@ */ import { ml } from '../../../../../services/ml_api_service'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; +import { extractErrorProperties } from '../../../../../../../common/util/errors'; import { DfAnalyticsExplainResponse, FieldSelectionItem } from '../../../../common/analytics'; import { getJobConfigFromFormState, @@ -23,6 +23,7 @@ export interface FetchExplainDataReturnType { export const fetchExplainData = async (formState: State['form']) => { const jobConfig = getJobConfigFromFormState(formState); let errorMessage = ''; + let errorReason = ''; let success = true; let expectedMemory = ''; let fieldSelection: FieldSelectionItem[] = []; @@ -36,8 +37,12 @@ export const fetchExplainData = async (formState: State['form']) => { expectedMemory = resp.memory_estimation?.expected_memory_without_disk; fieldSelection = resp.field_selection || []; } catch (error) { + const errObj = extractErrorProperties(error); success = false; - errorMessage = extractErrorMessage(error); + errorMessage = errObj.message; + if (errObj.causedBy) { + errorReason = errObj.causedBy; + } } return { @@ -45,5 +50,6 @@ export const fetchExplainData = async (formState: State['form']) => { expectedMemory, fieldSelection, errorMessage, + errorReason, }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx index ee66612de97a..2ce6e7ac0e33 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx @@ -121,6 +121,30 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo optional: true, ignore: true, }, + alpha: { + optional: true, + formKey: 'alpha', + }, + downsample_factor: { + optional: true, + formKey: 'downsampleFactor', + }, + eta_growth_rate_per_tree: { + optional: true, + formKey: 'etaGrowthRatePerTree', + }, + max_optimization_rounds_per_hyperparameter: { + optional: true, + formKey: 'maxOptimizationRoundsPerHyperparameter', + }, + soft_tree_depth_limit: { + optional: true, + formKey: 'softTreeDepthLimit', + }, + soft_tree_depth_tolerance: { + optional: true, + formKey: 'softTreeDepthTolerance', + }, }, } : {}), @@ -215,6 +239,30 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo optional: true, ignore: true, }, + alpha: { + optional: true, + formKey: 'alpha', + }, + downsample_factor: { + optional: true, + formKey: 'downsampleFactor', + }, + eta_growth_rate_per_tree: { + optional: true, + formKey: 'etaGrowthRatePerTree', + }, + max_optimization_rounds_per_hyperparameter: { + optional: true, + formKey: 'maxOptimizationRoundsPerHyperparameter', + }, + soft_tree_depth_limit: { + optional: true, + formKey: 'softTreeDepthLimit', + }, + soft_tree_depth_tolerance: { + optional: true, + formKey: 'softTreeDepthTolerance', + }, }, } : {}), diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index a70962c45ffc..131da93a2328 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -47,6 +47,7 @@ export interface State { advancedEditorRawString: string; disableSwitchToForm: boolean; form: { + alpha: undefined | number; computeFeatureInfluence: string; createIndexPattern: boolean; dependentVariable: DependentVariable; @@ -57,7 +58,9 @@ export interface State { destinationIndexNameValid: boolean; destinationIndexPatternTitleExists: boolean; earlyStoppingEnabled: undefined | boolean; + downsampleFactor: undefined | number; eta: undefined | number; + etaGrowthRatePerTree: undefined | number; featureBagFraction: undefined | number; featureInfluenceThreshold: undefined | number; gamma: undefined | number; @@ -73,6 +76,7 @@ export interface State { lambda: number | undefined; loadingFieldOptions: boolean; maxNumThreads: undefined | number; + maxOptimizationRoundsPerHyperparameter: undefined | number; maxTrees: undefined | number; method: undefined | string; modelMemoryLimit: string | undefined; @@ -88,6 +92,8 @@ export interface State { requiredFieldsError: string | undefined; randomizeSeed: undefined | number; resultsField: undefined | string; + softTreeDepthLimit: undefined | number; + softTreeDepthTolerance: undefined | number; sourceIndex: EsIndexName; sourceIndexNameEmpty: boolean; sourceIndexNameValid: boolean; @@ -117,6 +123,7 @@ export const getInitialState = (): State => ({ advancedEditorRawString: '', disableSwitchToForm: false, form: { + alpha: undefined, computeFeatureInfluence: 'true', createIndexPattern: true, dependentVariable: '', @@ -127,7 +134,9 @@ export const getInitialState = (): State => ({ destinationIndexNameValid: false, destinationIndexPatternTitleExists: false, earlyStoppingEnabled: undefined, + downsampleFactor: undefined, eta: undefined, + etaGrowthRatePerTree: undefined, featureBagFraction: undefined, featureInfluenceThreshold: undefined, gamma: undefined, @@ -143,6 +152,7 @@ export const getInitialState = (): State => ({ lambda: undefined, loadingFieldOptions: false, maxNumThreads: DEFAULT_MAX_NUM_THREADS, + maxOptimizationRoundsPerHyperparameter: undefined, maxTrees: undefined, method: undefined, modelMemoryLimit: undefined, @@ -158,6 +168,8 @@ export const getInitialState = (): State => ({ requiredFieldsError: undefined, randomizeSeed: undefined, resultsField: undefined, + softTreeDepthLimit: undefined, + softTreeDepthTolerance: undefined, sourceIndex: '', sourceIndexNameEmpty: true, sourceIndexNameValid: false, @@ -233,17 +245,31 @@ export const getJobConfigFromFormState = ( analysis = Object.assign( analysis, - formState.predictionFieldName && { prediction_field_name: formState.predictionFieldName }, + formState.alpha && { alpha: formState.alpha }, formState.eta && { eta: formState.eta }, + formState.etaGrowthRatePerTree && { + eta_growth_rate_per_tree: formState.etaGrowthRatePerTree, + }, + formState.downsampleFactor && { downsample_factor: formState.downsampleFactor }, formState.featureBagFraction && { feature_bag_fraction: formState.featureBagFraction, }, formState.gamma && { gamma: formState.gamma }, formState.lambda && { lambda: formState.lambda }, + formState.maxOptimizationRoundsPerHyperparameter && { + max_optimization_rounds_per_hyperparameter: + formState.maxOptimizationRoundsPerHyperparameter, + }, formState.maxTrees && { max_trees: formState.maxTrees }, formState.randomizeSeed && { randomize_seed: formState.randomizeSeed }, formState.earlyStoppingEnabled !== undefined && { early_stopping_enabled: formState.earlyStoppingEnabled, + }, + formState.predictionFieldName && { prediction_field_name: formState.predictionFieldName }, + formState.randomizeSeed && { randomize_seed: formState.randomizeSeed }, + formState.softTreeDepthLimit && { soft_tree_depth_limit: formState.softTreeDepthLimit }, + formState.softTreeDepthTolerance && { + soft_tree_depth_tolerance: formState.softTreeDepthTolerance, } ); From ec8305baa7c81587028caed5db7042e5a02c2e28 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Wed, 10 Feb 2021 10:01:49 -0600 Subject: [PATCH 10/32] [docs] Include setup instructions for environment variable KBN_PATH_CONF (#90794) --- docs/setup/install/deb.asciidoc | 1 + docs/setup/install/rpm.asciidoc | 1 + docs/setup/install/targz.asciidoc | 1 + docs/setup/install/windows.asciidoc | 1 + docs/setup/settings.asciidoc | 7 ++++++- 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/setup/install/deb.asciidoc b/docs/setup/install/deb.asciidoc index 6012ae394c83..1ec73e8c3c7f 100644 --- a/docs/setup/install/deb.asciidoc +++ b/docs/setup/install/deb.asciidoc @@ -156,6 +156,7 @@ locations for a Debian-based system: | config | Configuration files including `kibana.yml` | /etc/kibana + | <> d| | data diff --git a/docs/setup/install/rpm.asciidoc b/docs/setup/install/rpm.asciidoc index 216ec849147b..a87d2f89b6dd 100644 --- a/docs/setup/install/rpm.asciidoc +++ b/docs/setup/install/rpm.asciidoc @@ -149,6 +149,7 @@ locations for an RPM-based system: | config | Configuration files including `kibana.yml` | /etc/kibana + | <> d| | data diff --git a/docs/setup/install/targz.asciidoc b/docs/setup/install/targz.asciidoc index bb51d98a4f92..f0a90723a8ed 100644 --- a/docs/setup/install/targz.asciidoc +++ b/docs/setup/install/targz.asciidoc @@ -134,6 +134,7 @@ important data later on. | config | Configuration files including `kibana.yml` | $KIBANA_HOME\config + | <> d| | data diff --git a/docs/setup/install/windows.asciidoc b/docs/setup/install/windows.asciidoc index b4204cc623f0..4138fc1886a6 100644 --- a/docs/setup/install/windows.asciidoc +++ b/docs/setup/install/windows.asciidoc @@ -81,6 +81,7 @@ important data later on. | config | Configuration files including `kibana.yml` | $KIBANA_HOME\config + | <> d| | data diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 9b9c26fd0e1d..b57152646dda 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -5,7 +5,12 @@ The {kib} server reads properties from the `kibana.yml` file on startup. The location of this file differs depending on how you installed {kib}. For example, if you installed {kib} from an archive distribution (`.tar.gz` or `.zip`), by default it is in `$KIBANA_HOME/config`. By default, with package distributions -(Debian or RPM), it is in `/etc/kibana`. +(Debian or RPM), it is in `/etc/kibana`. The config directory can be changed via the +`KBN_PATH_CONF` environment variable: + +``` +KBN_PATH_CONF=/home/kibana/config ./bin/kibana +``` The default host and port settings configure {kib} to run on `localhost:5601`. To change this behavior and allow remote users to connect, you'll need to update your `kibana.yml` file. You can also enable SSL and set a variety of other options. Finally, environment variables can be injected into From 0fc9467e5101b408573840a50f14f9ec817e1d6f Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 10 Feb 2021 17:08:38 +0100 Subject: [PATCH 11/32] fix popover state (#90942) --- .../indexpattern_datasource/dimension_panel/time_scaling.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx index 0dd54118d030..a9362060b2dd 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx @@ -90,7 +90,7 @@ export function TimeScaling({ iconSide="right" data-test-subj="indexPattern-time-scaling-popover" onClick={() => { - setPopoverOpen(true); + setPopoverOpen(!popoverOpen); }} > {i18n.translate('xpack.lens.indexPattern.timeScale.advancedSettings', { From 874960e1c54dba901abc64ffd1eca8faf784afc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Wed, 10 Feb 2021 17:13:05 +0100 Subject: [PATCH 12/32] [Logs UI] Handle undefined composite key in category dataset response (#90472) This fixes the handling of Elasticsearch responses that don't contain a composite key when querying for the available category datasets. --- .../log_analysis/log_entry_categories_datasets_stats.ts | 2 +- .../latest_log_entry_categories_datasets_stats.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_datasets_stats.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_datasets_stats.ts index 850fab6937f4..7c92a81e1a89 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_datasets_stats.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_datasets_stats.ts @@ -79,7 +79,7 @@ export async function getLatestLogEntriesCategoriesDatasetsStats( return { categorization_status: latestHitSource.categorization_status, categorized_doc_count: latestHitSource.categorized_doc_count, - dataset: bucket.key.dataset ?? '', + dataset: bucket.key?.dataset ?? '', dead_category_count: latestHitSource.dead_category_count, failed_category_count: latestHitSource.failed_category_count, frequent_category_count: latestHitSource.frequent_category_count, diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/latest_log_entry_categories_datasets_stats.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/latest_log_entry_categories_datasets_stats.ts index c7b2590e4be9..2cbb0ef60cd1 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/latest_log_entry_categories_datasets_stats.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/latest_log_entry_categories_datasets_stats.ts @@ -98,9 +98,12 @@ export const logEntryCategorizerStatsHitRT = rt.type({ export type LogEntryCategorizerStatsHit = rt.TypeOf; -const compositeDatasetKeyRT = rt.type({ - dataset: rt.union([rt.string, rt.null]), -}); +const compositeDatasetKeyRT = rt.union([ + rt.type({ + dataset: rt.union([rt.string, rt.null]), + }), + rt.undefined, +]); export type CompositeDatasetKey = rt.TypeOf; From f563a82903d96a74be5707c563fe105336571054 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Wed, 10 Feb 2021 11:15:21 -0500 Subject: [PATCH 13/32] [Security Solution] Use sourcerer selected indices in resolver (#90727) * Use sourcer indices * Add indices to panel requests * Use a separate indices selector for resolver events * Use valid timeline id in tests * Update TimelineId type usage, make selector test clearer * Update tests to use TimelineId type --- .../common/types/timeline/index.ts | 1 + .../events_viewer/events_viewer.test.tsx | 15 ++-- .../events_viewer/events_viewer.tsx | 2 +- .../components/events_viewer/index.test.tsx | 3 +- .../common/components/events_viewer/index.tsx | 3 +- .../public/resolver/store/data/reducer.ts | 2 + .../resolver/store/data/selectors.test.ts | 81 +++++++++++++++++++ .../public/resolver/store/data/selectors.ts | 27 ++++++- .../current_related_event_fetcher.ts | 2 +- .../store/middleware/node_data_fetcher.ts | 2 +- .../middleware/related_events_fetcher.ts | 2 +- .../public/resolver/store/selectors.ts | 7 +- .../public/resolver/types.ts | 2 + .../components/flyout/index.test.tsx | 3 +- .../timelines/components/flyout/index.tsx | 2 +- .../components/flyout/pane/index.test.tsx | 3 +- .../components/flyout/pane/index.tsx | 3 +- .../components/graph_overlay/index.test.tsx | 10 ++- .../components/graph_overlay/index.tsx | 30 ++++--- .../timeline/graph_tab_content/index.tsx | 3 +- .../components/timeline/index.test.tsx | 6 +- .../timelines/components/timeline/index.tsx | 4 +- .../timeline/tabs_content/index.tsx | 4 +- 23 files changed, 172 insertions(+), 45 deletions(-) diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 1ea9b5752e0c..26a30e7c8f23 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -280,6 +280,7 @@ export enum TimelineId { active = 'timeline-1', casePage = 'timeline-case', test = 'test', // Reserved for testing purposes + test2 = 'test2', } export const TimelineIdLiteralRt = runtimeTypes.union([ diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 4364ca2d3465..6dad6c439ce4 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -119,7 +119,7 @@ describe('EventsViewer', () => { let testProps = { defaultModel: eventsDefaultModel, end: to, - id: 'test-stateful-events-viewer', + id: TimelineId.test, start: from, scopeId: SourcererScopeName.timeline, }; @@ -155,7 +155,7 @@ describe('EventsViewer', () => { indexName: 'auditbeat-7.10.1-2020.12.18-000001', }, tabType: 'query', - timelineId: 'test-stateful-events-viewer', + timelineId: TimelineId.test, }, type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT', }); @@ -199,17 +199,22 @@ describe('EventsViewer', () => { defaultHeaders.forEach((header) => { test(`it renders the ${header.id} default EventsViewer column header`, () => { + testProps = { + ...testProps, + // Update with a new id, to force columns back to default. + id: TimelineId.test2, + }; const wrapper = mount( ); - defaultHeaders.forEach((h) => + defaultHeaders.forEach((h) => { expect(wrapper.find(`[data-test-subj="header-text-${header.id}"]`).first().exists()).toBe( true - ) - ); + ); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 77573dbab0a5..254309aee906 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -117,7 +117,7 @@ interface Props { filters: Filter[]; headerFilterGroup?: React.ReactNode; height?: number; - id: string; + id: TimelineId; indexNames: string[]; indexPattern: IIndexPattern; isLive: boolean; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index c2fbfdb666e0..5004c23f9111 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -16,6 +16,7 @@ import { useMountAppended } from '../../utils/use_mount_appended'; import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; import { eventsDefaultModel } from './default_model'; +import { TimelineId } from '../../../../common/types/timeline'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useTimelineEvents } from '../../../timelines/containers'; @@ -36,7 +37,7 @@ const testProps = { defaultModel: eventsDefaultModel, end: to, indexNames: [], - id: 'test-stateful-events-viewer', + id: TimelineId.test, scopeId: SourcererScopeName.default, start: from, }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 526bc312172b..2b5420674b89 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -12,6 +12,7 @@ import styled from 'styled-components'; import { inputsModel, inputsSelectors, State } from '../../store'; import { inputsActions } from '../../store/actions'; +import { TimelineId } from '../../../../common/types/timeline'; import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; import { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model'; import { Filter } from '../../../../../../../src/plugins/data/public'; @@ -34,7 +35,7 @@ const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` export interface OwnProps { defaultModel: SubsetTimelineModel; end: string; - id: string; + id: TimelineId; scopeId: SourcererScopeName; start: string; headerFilterGroup?: React.ReactNode; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index 8df0c92ca83e..b5864a0a83cf 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -19,6 +19,7 @@ const initialState: DataState = { data: null, }, resolverComponentInstanceID: undefined, + indices: [], }; /* eslint-disable complexity */ export const dataReducer: Reducer = (state = initialState, action) => { @@ -35,6 +36,7 @@ export const dataReducer: Reducer = (state = initialS }, resolverComponentInstanceID: action.payload.resolverComponentInstanceID, locationSearch: action.payload.locationSearch, + indices: action.payload.indices, }; const panelViewAndParameters = selectors.panelViewAndParameters(nextState); return { diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts index c372c98c6e06..b864bb254a5f 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts @@ -664,4 +664,85 @@ describe('data state', () => { `); }); }); + describe('when the resolver tree response is complete, still use non-default indices', () => { + beforeEach(() => { + const { resolverTree } = mockTreeWithNoAncestorsAnd2Children({ + originID: 'a', + firstChildID: 'b', + secondChildID: 'c', + }); + const { schema, dataSource } = endpointSourceSchema(); + actions = [ + { + type: 'serverReturnedResolverData', + payload: { + result: resolverTree, + dataSource, + schema, + parameters: { + databaseDocumentID: '', + indices: ['someNonDefaultIndex'], + filters: {}, + }, + }, + }, + ]; + }); + it('should have an empty array for tree parameter indices, and a non empty array for event indices', () => { + const treeParameterIndices = selectors.treeParameterIndices(state()); + expect(treeParameterIndices.length).toBe(0); + const eventIndices = selectors.eventIndices(state()); + expect(eventIndices.length).toBe(1); + }); + }); + describe('when the resolver tree response is pending use the same indices the user is currently looking at data from', () => { + beforeEach(() => { + const { resolverTree } = mockTreeWithNoAncestorsAnd2Children({ + originID: 'a', + firstChildID: 'b', + secondChildID: 'c', + }); + const { schema, dataSource } = endpointSourceSchema(); + actions = [ + { + type: 'serverReturnedResolverData', + payload: { + result: resolverTree, + dataSource, + schema, + parameters: { + databaseDocumentID: '', + indices: ['defaultIndex'], + filters: {}, + }, + }, + }, + { + type: 'appReceivedNewExternalProperties', + payload: { + databaseDocumentID: '', + resolverComponentInstanceID: '', + locationSearch: '', + indices: ['someNonDefaultIndex', 'someOtherIndex'], + shouldUpdate: false, + filters: {}, + }, + }, + { + type: 'appRequestedResolverData', + payload: { + databaseDocumentID: '', + indices: ['someNonDefaultIndex', 'someOtherIndex'], + filters: {}, + }, + }, + ]; + }); + it('should have an empty array for tree parameter indices, and the same set of indices as the last tree response', () => { + const treeParameterIndices = selectors.treeParameterIndices(state()); + expect(treeParameterIndices.length).toBe(0); + const eventIndices = selectors.eventIndices(state()); + expect(eventIndices.length).toBe(1); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index a39aa4f0cd98..fb6fb6073d7c 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -63,6 +63,13 @@ export function resolverComponentInstanceID(state: DataState): string { return state.resolverComponentInstanceID ? state.resolverComponentInstanceID : ''; } +/** + * The indices resolver should use, passed in as external props. + */ +const currentIndices = (state: DataState): string[] => { + return state.indices; +}; + /** * The last NewResolverTree we received, if any. It may be stale (it might not be for the same databaseDocumentID that * we're currently interested in. @@ -71,6 +78,12 @@ const resolverTreeResponse = (state: DataState): NewResolverTree | undefined => return state.tree?.lastResponse?.successful ? state.tree?.lastResponse.result : undefined; }; +const lastResponseIndices = (state: DataState): string[] | undefined => { + return state.tree?.lastResponse?.successful + ? state.tree?.lastResponse?.parameters?.indices + : undefined; +}; + /** * If we received a NewResolverTree, return the schema associated with that tree, otherwise return undefined. * As of writing, this is only used for the info popover in the graph_controls panel @@ -336,10 +349,22 @@ export const timeRangeFilters = createSelector( /** * The indices to use for the requests with the backend. */ -export const treeParamterIndices = createSelector(treeParametersToFetch, (parameters) => { +export const treeParameterIndices = createSelector(treeParametersToFetch, (parameters) => { return parameters?.indices ?? []; }); +/** + * Panel requests should not use indices derived from the tree parameter selector, as this is only defined briefly while the resolver_tree_fetcher middleware is running. + * Instead, panel requests should use the indices used by the last good request, falling back to the indices passed as external props. + */ +export const eventIndices = createSelector( + lastResponseIndices, + currentIndices, + function eventIndices(lastIndices, current): string[] { + return lastIndices ?? current ?? []; + } +); + export const layout: (state: DataState) => IsometricTaxiLayout = createSelector( tree, originID, diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts index 3b8389182e99..33772dddd676 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts @@ -32,7 +32,7 @@ export function CurrentRelatedEventFetcher( const state = api.getState(); const newParams = selectors.panelViewAndParameters(state); - const indices = selectors.treeParameterIndices(state); + const indices = selectors.eventIndices(state); const oldParams = last; last = newParams; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts index 696e7f921673..074fdf753579 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts @@ -38,7 +38,7 @@ export function NodeDataFetcher( * This gets the visible nodes that we haven't already requested or received data for */ const newIDsToRequest: Set = selectors.newIDsToRequest(state)(Number.POSITIVE_INFINITY); - const indices = selectors.treeParameterIndices(state); + const indices = selectors.eventIndices(state); if (newIDsToRequest.size <= 0) { return; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts index fbce03caf64d..19a11e07a9d8 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts @@ -27,7 +27,7 @@ export function RelatedEventsFetcher( const newParams = selectors.panelViewAndParameters(state); const isLoadingMoreEvents = selectors.isLoadingMoreNodeEventsInCategory(state); - const indices = selectors.treeParameterIndices(state); + const indices = selectors.eventIndices(state); const oldParams = last; const timeRangeFilters = selectors.timeRangeFilters(state); diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index a845de57bbdc..4c088a8be4ed 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -80,9 +80,14 @@ export const treeRequestParametersToAbort = composeSelectors( */ export const treeParameterIndices = composeSelectors( dataStateSelector, - dataSelectors.treeParamterIndices + dataSelectors.treeParameterIndices ); +/** + * An array of indices to use for resolver panel requests. + */ +export const eventIndices = composeSelectors(dataStateSelector, dataSelectors.eventIndices); + export const resolverComponentInstanceID = composeSelectors( dataStateSelector, dataSelectors.resolverComponentInstanceID diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index d3ddc51429cc..e6a004938a26 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -376,6 +376,8 @@ export interface DataState { */ readonly resolverComponentInstanceID?: string; + readonly indices: string[]; + /** * The `search` part of the URL. */ diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx index 3783f5591c43..f57ce42e7e07 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx @@ -18,6 +18,7 @@ import { kibanaObservable, createSecuritySolutionStorageMock, } from '../../../common/mock'; +import { TimelineId } from '../../../../common/types/timeline'; import { createStore, State } from '../../../common/store'; import * as timelineActions from '../../store/timeline/actions'; @@ -43,7 +44,7 @@ describe('Flyout', () => { const { storage } = createSecuritySolutionStorageMock(); const props = { onAppLeave: jest.fn(), - timelineId: 'test', + timelineId: TimelineId.test, }; beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx index f7518c2c34f6..bd7c7fbd1941 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx @@ -26,7 +26,7 @@ const Visible = styled.div<{ show?: boolean }>` Visible.displayName = 'Visible'; interface OwnProps { - timelineId: string; + timelineId: TimelineId; onAppLeave: (handler: AppLeaveHandler) => void; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx index e16cec78cf13..4ccc7ef5b5bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.test.tsx @@ -9,13 +9,14 @@ import { shallow } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../../../common/mock'; +import { TimelineId } from '../../../../../common/types/timeline'; import { Pane } from '.'; describe('Pane', () => { test('renders correctly against snapshot', () => { const EmptyComponent = shallow( - + ); expect(EmptyComponent.find('Pane')).toMatchSnapshot(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index a4d85bd76b10..e63ffedf3da7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -11,12 +11,13 @@ import styled from 'styled-components'; import { useDispatch } from 'react-redux'; import { StatefulTimeline } from '../../timeline'; +import { TimelineId } from '../../../../../common/types/timeline'; import * as i18n from './translations'; import { timelineActions } from '../../../store/timeline'; import { focusActiveTimelineButton } from '../../timeline/helpers'; interface FlyoutPaneComponentProps { - timelineId: string; + timelineId: TimelineId; } const EuiFlyoutContainer = styled.div` diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx index c0e1a54faa8d..1286208bff9e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx @@ -15,7 +15,6 @@ import { } from '../../../common/containers/use_full_screen'; import { mockTimelineModel, TestProviders } from '../../../common/mock'; import { TimelineId } from '../../../../common/types/timeline'; - import { GraphOverlay } from '.'; jest.mock('../../../common/hooks/use_selector', () => ({ @@ -28,6 +27,10 @@ jest.mock('../../../common/containers/use_full_screen', () => ({ useTimelineFullScreen: jest.fn(), })); +jest.mock('../../../resolver/view/use_resolver_query_params_cleaner'); +jest.mock('../../../resolver/view/use_state_syncing_actions'); +jest.mock('../../../resolver/view/use_sync_selected_node'); + describe('GraphOverlay', () => { beforeEach(() => { (useGlobalFullScreen as jest.Mock).mockReturnValue({ @@ -42,12 +45,11 @@ describe('GraphOverlay', () => { describe('when used in an events viewer (i.e. in the Detections view, or the Host > Events view)', () => { const isEventViewer = true; - const timelineId = 'used-as-an-events-viewer'; test('it has 100% width when isEventViewer is true and NOT in full screen mode', async () => { const wrapper = mount( - + ); @@ -69,7 +71,7 @@ describe('GraphOverlay', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 1b3a0c21ef68..9c9c56461609 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -20,7 +20,7 @@ import styled from 'styled-components'; import { FULL_SCREEN } from '../timeline/body/column_headers/translations'; import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations'; -import { DEFAULT_INDEX_KEY, FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; +import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; import { useGlobalFullScreen, useTimelineFullScreen, @@ -30,6 +30,8 @@ import { TimelineId } from '../../../../common/types/timeline'; import { timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../store/timeline/defaults'; import { isFullScreen } from '../timeline/body/column_headers'; +import { useSourcererScope } from '../../../common/containers/sourcerer'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { updateTimelineGraphEventId } from '../../../timelines/store/timeline/actions'; import { Resolver } from '../../../resolver/view'; import { @@ -38,8 +40,6 @@ import { endSelector, } from '../../../common/components/super_date_picker/selectors'; import * as i18n from './translations'; -import { useUiSetting$ } from '../../../common/lib/kibana'; -import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; const OverlayContainer = styled.div` ${({ $restrictWidth }: { $restrictWidth: boolean }) => @@ -61,14 +61,14 @@ const FullScreenButtonIcon = styled(EuiButtonIcon)` interface OwnProps { isEventViewer: boolean; - timelineId: string; + timelineId: TimelineId; } interface NavigationProps { fullScreen: boolean; globalFullScreen: boolean; onCloseOverlay: () => void; - timelineId: string; + timelineId: TimelineId; timelineFullScreen: boolean; toggleFullScreen: () => void; } @@ -169,16 +169,14 @@ const GraphOverlayComponent: React.FC = ({ isEventViewer, timelineId } globalFullScreen, ]); - const { signalIndexName } = useSignalIndex(); - const [siemDefaultIndices] = useUiSetting$(DEFAULT_INDEX_KEY); - const indices: string[] | null = useMemo(() => { - if (signalIndexName === null) { - return null; - } else { - return [...siemDefaultIndices, signalIndexName]; - } - }, [signalIndexName, siemDefaultIndices]); + let sourcereScope = SourcererScopeName.default; + if ([TimelineId.detectionsRulesDetailsPage, TimelineId.detectionsPage].includes(timelineId)) { + sourcereScope = SourcererScopeName.detections; + } else if (timelineId === TimelineId.active) { + sourcereScope = SourcererScopeName.timeline; + } + const { selectedPatterns } = useSourcererScope(sourcereScope); return ( = ({ isEventViewer, timelineId } - {graphEventId !== undefined && indices !== null ? ( + {graphEventId !== undefined ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx index db4867e1abfe..1678a92c4cda 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx @@ -9,10 +9,11 @@ import React, { useMemo } from 'react'; import { timelineSelectors } from '../../../store/timeline'; import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { TimelineId } from '../../../../../common/types/timeline'; import { GraphOverlay } from '../../graph_overlay'; interface GraphTabContentProps { - timelineId: string; + timelineId: TimelineId; } const GraphTabContentComponent: React.FC = ({ timelineId }) => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index 219d32f147b6..e7422e32805a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -12,7 +12,7 @@ import useResizeObserver from 'use-resize-observer/polyfilled'; import { DragDropContextWrapper } from '../../../common/components/drag_and_drop/drag_drop_context_wrapper'; import '../../../common/mock/match_media'; import { mockBrowserFields, mockDocValueFields } from '../../../common/containers/source/mock'; - +import { TimelineId } from '../../../../common/types/timeline'; import { mockIndexNames, mockIndexPattern, TestProviders } from '../../../common/mock'; import { StatefulTimeline, Props as StatefulTimelineOwnProps } from './index'; @@ -55,7 +55,7 @@ jest.mock('../../../common/containers/sourcerer', () => { }); describe('StatefulTimeline', () => { const props: StatefulTimelineOwnProps = { - timelineId: 'timeline-test', + timelineId: TimelineId.test, }; beforeEach(() => { @@ -91,7 +91,7 @@ describe('StatefulTimeline', () => { ); expect( wrapper - .find(`[data-timeline-id="timeline-test"].${SELECTOR_TIMELINE_GLOBAL_CONTAINER}`) + .find(`[data-timeline-id="test"].${SELECTOR_TIMELINE_GLOBAL_CONTAINER}`) .first() .exists() ).toEqual(true); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 9cb95daba685..c37fc93e33b0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -18,7 +18,7 @@ import { isTab } from '../../../common/components/accessibility/helpers'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { FlyoutHeader, FlyoutHeaderPanel } from '../flyout/header'; -import { TimelineType, TimelineTabs } from '../../../../common/types/timeline'; +import { TimelineType, TimelineTabs, TimelineId } from '../../../../common/types/timeline'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { activeTimeline } from '../../containers/active_timeline_context'; import { EVENTS_COUNT_BUTTON_CLASS_NAME, onTimelineTabKeyPressed } from './helpers'; @@ -35,7 +35,7 @@ const TimelineTemplateBadge = styled.div` `; export interface Props { - timelineId: string; + timelineId: TimelineId; } const TimelineSavingProgressComponent: React.FC = ({ timelineId }) => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index 9f6bfcf7e320..ca70e4ae6468 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -9,7 +9,7 @@ import { EuiBadge, EuiLoadingContent, EuiTabs, EuiTab } from '@elastic/eui'; import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import { TimelineTabs } from '../../../../../common/types/timeline'; +import { TimelineTabs, TimelineId } from '../../../../../common/types/timeline'; import { useShallowEqualSelector, @@ -42,7 +42,7 @@ const NotesTabContent = lazy(() => import('../notes_tab_content')); const PinnedTabContent = lazy(() => import('../pinned_tab_content')); interface BasicTimelineTab { - timelineId: string; + timelineId: TimelineId; graphEventId?: string; } From d1653bc42583ba1ced34b59c7408f719713c08ab Mon Sep 17 00:00:00 2001 From: Jessica David Date: Wed, 10 Feb 2021 11:26:58 -0500 Subject: [PATCH 14/32] Adding new fields to the allowlist for alert telemetry (#90868) --- .../security_solution/common/endpoint/generate_data.ts | 2 ++ .../security_solution/server/lib/telemetry/sender.test.ts | 4 ++++ .../plugins/security_solution/server/lib/telemetry/sender.ts | 2 ++ 3 files changed, 8 insertions(+) diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index ba64814cd1da..ffeaf853828f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -558,6 +558,8 @@ export class EndpointDocGenerator { version: '3.0.33', }, temp_file_path: 'C:/temp/fake_malware.exe', + quarantine_result: true, + quarantine_message: 'fake quarantine message', }, }, process: { diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 8af0df306302..56e2f9c7c730 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -47,6 +47,8 @@ describe('TelemetryEventsSender', () => { malware_classification: { key1: 'X', }, + quarantine_result: true, + quarantine_message: 'this file is bad', something_else: 'nope', }, }, @@ -79,6 +81,8 @@ describe('TelemetryEventsSender', () => { malware_classification: { key1: 'X', }, + quarantine_result: true, + quarantine_message: 'this file is bad', }, }, host: { diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index 4e32410cdb6a..a18604fb92a4 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -320,6 +320,8 @@ const allowlistEventFields: AllowlistFields = { Ext: { code_signature: true, malware_classification: true, + quarantine_result: true, + quarantine_message: true, }, }, host: { From c058d9b02422a87bbd4c263e51b3e8509ff205ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Wed, 10 Feb 2021 17:48:42 +0100 Subject: [PATCH 15/32] [Asset Management] Migrate Osquery plugin to TS project references (#90916) --- tsconfig.refs.json | 1 + x-pack/plugins/osquery/kibana.json | 3 +-- x-pack/plugins/osquery/tsconfig.json | 34 ++++++++++++++++++++++++++++ x-pack/test/tsconfig.json | 1 + x-pack/tsconfig.json | 2 ++ 5 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/osquery/tsconfig.json diff --git a/tsconfig.refs.json b/tsconfig.refs.json index d5482a85856f..4105f23fd5b3 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -87,6 +87,7 @@ { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, { "path": "./x-pack/plugins/observability/tsconfig.json" }, + { "path": "./x-pack/plugins/osquery/tsconfig.json" }, { "path": "./x-pack/plugins/painless_lab/tsconfig.json" }, { "path": "./x-pack/plugins/reporting/tsconfig.json" }, { "path": "./x-pack/plugins/saved_objects_tagging/tsconfig.json" }, diff --git a/x-pack/plugins/osquery/kibana.json b/x-pack/plugins/osquery/kibana.json index f6e90b946050..8adb30f4271d 100644 --- a/x-pack/plugins/osquery/kibana.json +++ b/x-pack/plugins/osquery/kibana.json @@ -14,8 +14,7 @@ "requiredBundles": [ "esUiShared", "kibanaUtils", - "kibanaReact", - "kibanaUtils" + "kibanaReact" ], "requiredPlugins": [ "data", diff --git a/x-pack/plugins/osquery/tsconfig.json b/x-pack/plugins/osquery/tsconfig.json new file mode 100644 index 000000000000..616783376258 --- /dev/null +++ b/x-pack/plugins/osquery/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + // add all the folders contains files to be compiled + "common/**/*", + "public/**/*", + "server/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + // add references to other TypeScript projects the plugin depends on + + // requiredPlugins from ./kibana.json + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../data_enhanced/tsconfig.json" }, + { "path": "../fleet/tsconfig.json" }, + + // optionalPlugins from ./kibana.json + { "path": "../../../src/plugins/home/tsconfig.json" }, + + // requiredBundles from ./kibana.json + { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 4cbec2da2180..6209503e7561 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -68,6 +68,7 @@ { "path": "../plugins/licensing/tsconfig.json" }, { "path": "../plugins/ml/tsconfig.json" }, { "path": "../plugins/observability/tsconfig.json" }, + { "path": "../plugins/osquery/tsconfig.json" }, { "path": "../plugins/painless_lab/tsconfig.json" }, { "path": "../plugins/runtime_fields/tsconfig.json" }, { "path": "../plugins/saved_objects_tagging/tsconfig.json" }, diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 00286ac47da6..5589c62010db 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -30,6 +30,7 @@ "plugins/maps_legacy_licensing/**/*", "plugins/ml/**/*", "plugins/observability/**/*", + "plugins/osquery/**/*", "plugins/reporting/**/*", "plugins/searchprofiler/**/*", "plugins/security_solution/cypress/**/*", @@ -133,6 +134,7 @@ { "path": "./plugins/maps/tsconfig.json" }, { "path": "./plugins/ml/tsconfig.json" }, { "path": "./plugins/observability/tsconfig.json" }, + { "path": "./plugins/osquery/tsconfig.json" }, { "path": "./plugins/painless_lab/tsconfig.json" }, { "path": "./plugins/saved_objects_tagging/tsconfig.json" }, { "path": "./plugins/searchprofiler/tsconfig.json" }, From 4881306419e401b774143063f9c29caff7c3ca9d Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Wed, 10 Feb 2021 17:22:37 +0000 Subject: [PATCH 16/32] [ML] Stops new line on enter key press for KQL query bars (#90960) --- .../components/exploration_query_bar/exploration_query_bar.tsx | 2 +- .../index_based/components/search_panel/search_panel.tsx | 2 +- .../components/explorer_query_bar/explorer_query_bar.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx index a017de8d43a7..b079fc154713 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx @@ -150,7 +150,7 @@ export const ExplorationQueryBar: FC = ({ = ({ closePopover={() => setErrorMessage(undefined)} input={ = ({ closePopover={() => setErrorMessage(undefined)} input={ Date: Wed, 10 Feb 2021 12:19:13 -0600 Subject: [PATCH 17/32] Remove custom plot plugins when Canvas is unmounted (#90722) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/canvas/public/application.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 0e5300eeb1b0..66b02bdc1640 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -13,6 +13,8 @@ import { i18n } from '@kbn/i18n'; import { Provider } from 'react-redux'; import { BehaviorSubject } from 'rxjs'; +import { includes, remove } from 'lodash'; + import { AppMountParameters, CoreStart, CoreSetup, AppUpdater } from 'kibana/public'; import { CanvasStartDeps, CanvasSetupDeps } from './plugin'; @@ -39,6 +41,11 @@ import { initFunctions } from './functions'; // @ts-expect-error untyped local import { appUnload } from './state/actions/app'; +// @ts-expect-error Not going to convert +import { size } from '../canvas_plugin_src/renderers/plot/plugins/size'; +// @ts-expect-error Not going to convert +import { text } from '../canvas_plugin_src/renderers/plot/plugins/text'; + import './style/index.scss'; const { ReadOnlyBadge: strings } = CapabilitiesStrings; @@ -147,6 +154,17 @@ export const initializeCanvas = async ( export const teardownCanvas = (coreStart: CoreStart, startPlugins: CanvasStartDeps) => { destroyRegistries(); + // Canvas pollutes the jQuery plot plugins collection with custom plugins that only work in Canvas. + // Remove them when Canvas is unmounted. + // see: ../canvas_plugin_src/renderers/plot/plugins/index.ts + if (includes($.plot.plugins, size)) { + remove($.plot.plugins, size); + } + + if (includes($.plot.plugins, text)) { + remove($.plot.plugins, text); + } + // TODO: Not cleaning these up temporarily. // We have an issue where if requests are inflight, and you navigate away, // those requests could still be trying to act on the store and possibly require services. From 4f38d163bf9dda16634bf17d400cfbd6507657b6 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 10 Feb 2021 10:45:32 -0800 Subject: [PATCH 18/32] ignore CI Stats failures in flaky test jobs (#90999) Co-authored-by: spalger --- .ci/Jenkinsfile_flaky | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index 33204d739646..b9880c410fc6 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -29,16 +29,20 @@ kibanaPipeline(timeoutMinutes: 180) { catchErrors { print "Agent ${agentNumberInside} - ${agentExecutions} executions" - workers.functional('flaky-test-runner', { - if (!IS_XPACK) { - kibanaPipeline.buildOss() - if (CI_GROUP == '1') { - runbld("./test/scripts/jenkins_build_kbn_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + withEnv([ + 'IGNORE_SHIP_CI_STATS_ERROR=true', + ]) { + workers.functional('flaky-test-runner', { + if (!IS_XPACK) { + kibanaPipeline.buildOss() + if (CI_GROUP == '1') { + runbld("./test/scripts/jenkins_build_kbn_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") + } + } else { + kibanaPipeline.buildXpack() } - } else { - kibanaPipeline.buildXpack() - } - }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() + }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() + } } } } From 4ee5f094ce7f6a6c480e631fe2ffe562a2e3d86e Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Wed, 10 Feb 2021 12:48:32 -0600 Subject: [PATCH 19/32] [Time to Visualize] Add functional tests for adding visualizations from Visualize, Lens, and Maps and adjust capabilities for new modal (#89245) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/dashboard_picker.tsx | 1 + .../saved_object_save_modal_dashboard.tsx | 12 +- ...d_object_save_modal_dashboard_selector.tsx | 84 +++--- .../public/services/index.ts | 1 - .../public/services/kibana/capabilities.ts | 1 - .../components/visualize_listing.tsx | 1 + .../apps/visualize/_add_to_dashboard.ts | 147 +++++++++++ test/functional/apps/visualize/index.ts | 1 + test/functional/page_objects/index.ts | 2 + .../page_objects/time_to_visualize_page.ts | 101 ++++++++ .../functional/page_objects/visualize_page.ts | 1 + .../functional/apps/dashboard/sync_colors.ts | 4 +- .../functional/apps/lens/add_to_dashboard.ts | 243 ++++++++++++++++++ x-pack/test/functional/apps/lens/index.ts | 1 + .../apps/maps/embeddable/add_to_dashboard.js | 122 +++++++++ .../functional/apps/maps/embeddable/index.js | 1 + .../test/functional/page_objects/lens_page.ts | 16 +- 17 files changed, 676 insertions(+), 63 deletions(-) create mode 100644 test/functional/apps/visualize/_add_to_dashboard.ts create mode 100644 test/functional/page_objects/time_to_visualize_page.ts create mode 100644 x-pack/test/functional/apps/lens/add_to_dashboard.ts create mode 100644 x-pack/test/functional/apps/maps/embeddable/add_to_dashboard.js diff --git a/src/plugins/presentation_util/public/components/dashboard_picker.tsx b/src/plugins/presentation_util/public/components/dashboard_picker.tsx index 6667280d0a23..83ccabe46cdc 100644 --- a/src/plugins/presentation_util/public/components/dashboard_picker.tsx +++ b/src/plugins/presentation_util/public/components/dashboard_picker.tsx @@ -64,6 +64,7 @@ export function DashboardPicker(props: DashboardPickerProps) { return ( ( documentId || disableDashboardOptions ? null : 'existing' diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx index 9f6fd5eabf5c..c2b5eac4dbb8 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx @@ -21,7 +21,6 @@ import { EuiSpacer, } from '@elastic/eui'; -import { pluginServices } from '../services'; import { DashboardPicker, DashboardPickerProps } from './dashboard_picker'; import './saved_object_save_modal_dashboard.scss'; @@ -37,9 +36,6 @@ export interface SaveModalDashboardSelectorProps { export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProps) { const { documentId, onSelectDashboard, dashboardOption, onChange, copyOnSave } = props; - const { capabilities } = pluginServices.getHooks(); - const { canCreateNewDashboards, canEditDashboards } = capabilities.useService(); - const isDisabled = !copyOnSave && !!documentId; return ( @@ -70,50 +66,44 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp >
- {canEditDashboards() && ( - <> - {' '} - onChange('existing')} - disabled={isDisabled} - /> -
- -
- - - )} - {canCreateNewDashboards() && ( - <> - {' '} - onChange('new')} - disabled={isDisabled} + <> + onChange('existing')} + disabled={isDisabled} + /> +
+ - - - )} +
+ + + <> + onChange('new')} + disabled={isDisabled} + /> + + boolean; canCreateNewDashboards: () => boolean; - canEditDashboards: () => boolean; } export interface PresentationUtilServices { diff --git a/src/plugins/presentation_util/public/services/kibana/capabilities.ts b/src/plugins/presentation_util/public/services/kibana/capabilities.ts index a191e970591f..546281d083f2 100644 --- a/src/plugins/presentation_util/public/services/kibana/capabilities.ts +++ b/src/plugins/presentation_util/public/services/kibana/capabilities.ts @@ -21,6 +21,5 @@ export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ coreSta return { canAccessDashboards: () => Boolean(dashboard.show), canCreateNewDashboards: () => Boolean(dashboard.createNew), - canEditDashboards: () => !Boolean(dashboard.hideWriteControls), }; }; diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index bc766d63db5a..1f1f8c0b5ac8 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -149,6 +149,7 @@ export const VisualizeListing = () => { const calloutMessage = ( <> { + it('should allow new lens vizs be added to a new dashboard', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + + await PageObjects.lens.switchToVisualization('lnsMetric'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + + await PageObjects.lens.save('New Lens from Modal', false, false, 'new'); + + await PageObjects.dashboard.waitForRenderComplete(); + + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('should allow existing lens vizs be added to a new dashboard', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('Artistpreviouslyknownaslens'); + await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); + + await PageObjects.lens.save('Artistpreviouslyknownaslens Copy', true, false, 'new'); + + await PageObjects.dashboard.waitForRenderComplete(); + + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('should allow new lens vizs be added to an existing dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsXYvis'); + await find.clickByButtonText('lnsXYvis'); + await dashboardAddPanel.closeAddPanel(); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.dashboard.saveDashboard('My Very Cool Dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.searchAndExpectItemsCount('dashboard', 'My Very Cool Dashboard', 1); + + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + + await PageObjects.lens.switchToVisualization('lnsMetric'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + + await PageObjects.lens.save( + 'New Lens from Modal', + false, + false, + 'existing', + 'My Very Cool Dashboard' + ); + + await PageObjects.dashboard.waitForRenderComplete(); + + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(2); + }); + + it('should allow existing lens vizs be added to an existing dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsXYvis'); + await find.clickByButtonText('lnsXYvis'); + await dashboardAddPanel.closeAddPanel(); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.dashboard.saveDashboard('My Wonderful Dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.searchAndExpectItemsCount('dashboard', 'My Wonderful Dashboard', 1); + + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('Artistpreviouslyknownaslens'); + await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); + + await PageObjects.lens.save( + 'Artistpreviouslyknownaslens Copy', + true, + false, + 'existing', + 'My Wonderful Dashboard' + ); + + await PageObjects.dashboard.waitForRenderComplete(); + + await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(2); + }); + + describe('Capabilities', function capabilitiesTests() { + describe('dashboard no-access privileges', () => { + before(async () => { + await PageObjects.common.navigateToApp('visualize'); + await security.testUser.setRoles(['test_logstash_reader', 'global_visualize_all'], true); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should not display dashboard flow prompt', async () => { + await PageObjects.common.navigateToApp('visualize'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.visualize.gotoLandingPage(); + + const hasPrompt = await testSubjects.exists('visualize-dashboard-flow-prompt'); + expect(hasPrompt).to.eql(false); + }); + + it('should not display add-to-dashboard options', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + + await PageObjects.lens.switchToVisualization('lnsMetric'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.click('lnsApp_saveButton'); + + const hasOptions = await testSubjects.exists('add-to-dashboard-options'); + expect(hasOptions).to.eql(false); + }); + }); + + describe('dashboard read-only privileges', () => { + before(async () => { + await security.testUser.setRoles( + ['test_logstash_reader', 'global_visualize_all', 'global_dashboard_read'], + true + ); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should not display dashboard flow prompt', async () => { + await PageObjects.common.navigateToApp('visualize'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.visualize.gotoLandingPage(); + + const hasPrompt = await testSubjects.exists('visualize-dashboard-flow-prompt'); + expect(hasPrompt).to.eql(false); + }); + + it('should not display add-to-dashboard options', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'avg', + field: 'bytes', + }); + + await PageObjects.lens.switchToVisualization('lnsMetric'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.click('lnsApp_saveButton'); + + const hasOptions = await testSubjects.exists('add-to-dashboard-options'); + expect(hasOptions).to.eql(false); + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index 10b1f4d30145..31b7b665fb2f 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -29,6 +29,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { this.tags(['ciGroup4', 'skipFirefox']); loadTestFile(require.resolve('./smokescreen')); + loadTestFile(require.resolve('./add_to_dashboard')); loadTestFile(require.resolve('./table')); loadTestFile(require.resolve('./dashboard')); loadTestFile(require.resolve('./persistent_context')); diff --git a/x-pack/test/functional/apps/maps/embeddable/add_to_dashboard.js b/x-pack/test/functional/apps/maps/embeddable/add_to_dashboard.js new file mode 100644 index 000000000000..9bbf6b1afab6 --- /dev/null +++ b/x-pack/test/functional/apps/maps/embeddable/add_to_dashboard.js @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +export default function ({ getPageObjects, getService }) { + const PageObjects = getPageObjects([ + 'common', + 'dashboard', + 'header', + 'maps', + 'timeToVisualize', + 'visualize', + ]); + + const listingTable = getService('listingTable'); + const testSubjects = getService('testSubjects'); + const security = getService('security'); + + describe('maps add-to-dashboard save flow', () => { + before(async () => { + await security.testUser.setRoles( + [ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_dashboard_all', + 'meta_for_geoshape_data_reader', + ], + false + ); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should allow new map be added to a new dashboard', async () => { + await PageObjects.maps.openNewMap(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + + await testSubjects.click('mapSaveButton'); + await PageObjects.timeToVisualize.saveFromModal('map 1', { addToDashboard: 'new' }); + + await PageObjects.dashboard.waitForRenderComplete(); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('should allow existing maps be added to a new dashboard', async () => { + await PageObjects.maps.loadSavedMap('document example'); + + await testSubjects.click('mapSaveButton'); + await PageObjects.timeToVisualize.saveFromModal('document example copy', { + addToDashboard: 'new', + saveAsNew: true, + }); + + await PageObjects.dashboard.waitForRenderComplete(); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('should allow new map be added to an existing dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + + await PageObjects.dashboard.saveDashboard('My Very Cool Dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.searchAndExpectItemsCount('dashboard', 'My Very Cool Dashboard', 1); + + await PageObjects.maps.openNewMap(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + + await testSubjects.click('mapSaveButton'); + await PageObjects.timeToVisualize.saveFromModal('My New Map 2', { + addToDashboard: 'existing', + dashboardId: 'My Very Cool Dashboard', + }); + + await PageObjects.dashboard.waitForRenderComplete(); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + }); + + it('should allow existing maps be added to an existing dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + + await PageObjects.dashboard.saveDashboard('My Wonderful Dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.searchAndExpectItemsCount('dashboard', 'My Wonderful Dashboard', 1); + + await PageObjects.maps.loadSavedMap('document example'); + + await testSubjects.click('mapSaveButton'); + await PageObjects.timeToVisualize.saveFromModal('document example copy 2', { + addToDashboard: 'existing', + dashboardId: 'My Wonderful Dashboard', + saveAsNew: true, + }); + + await PageObjects.dashboard.waitForRenderComplete(); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/embeddable/index.js b/x-pack/test/functional/apps/maps/embeddable/index.js index 9fd4c9db703d..552f830e2a37 100644 --- a/x-pack/test/functional/apps/maps/embeddable/index.js +++ b/x-pack/test/functional/apps/maps/embeddable/index.js @@ -7,6 +7,7 @@ export default function ({ loadTestFile }) { describe('embeddable', function () { + loadTestFile(require.resolve('./add_to_dashboard')); loadTestFile(require.resolve('./save_and_return')); loadTestFile(require.resolve('./dashboard')); loadTestFile(require.resolve('./embeddable_library')); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index add6979c2dde..dcb730f77725 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -17,7 +17,15 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont const find = getService('find'); const comboBox = getService('comboBox'); const browser = getService('browser'); - const PageObjects = getPageObjects(['header', 'timePicker', 'common', 'visualize', 'dashboard']); + + const PageObjects = getPageObjects([ + 'header', + 'timePicker', + 'common', + 'visualize', + 'dashboard', + 'timeToVisualize', + ]); return logWrapper('lensPage', log, { /** @@ -341,16 +349,16 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont title: string, saveAsNew?: boolean, redirectToOrigin?: boolean, - addToDashboard?: boolean, + addToDashboard?: 'new' | 'existing' | null, dashboardId?: string ) { await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.click('lnsApp_saveButton'); - await PageObjects.visualize.setSaveModalValues(title, { + await PageObjects.timeToVisualize.setSaveModalValues(title, { saveAsNew, redirectToOrigin, - addToDashboard, + addToDashboard: addToDashboard ? addToDashboard : null, dashboardId, }); From 9778060ae81d29644b543b27bac5e3e6ce4ae82f Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 10 Feb 2021 14:10:28 -0500 Subject: [PATCH 20/32] Don't register management section if capabilities are not granted (#90782) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/constants/index.ts | 2 +- .../common/constants/plugin.ts | 1 + .../beats_management/public/bootstrap.tsx | 11 +++- .../framework/kibana_framework_adapter.ts | 3 +- x-pack/plugins/logstash/public/plugin.ts | 61 +++++++++++-------- 5 files changed, 48 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/beats_management/common/constants/index.ts b/x-pack/plugins/beats_management/common/constants/index.ts index a94c3614ae7a..ac4f89b639c2 100644 --- a/x-pack/plugins/beats_management/common/constants/index.ts +++ b/x-pack/plugins/beats_management/common/constants/index.ts @@ -7,7 +7,7 @@ export { UNIQUENESS_ENFORCING_TYPES } from './configuration_blocks'; export { INDEX_NAMES } from './index_names'; -export { PLUGIN } from './plugin'; +export { PLUGIN, MANAGEMENT_SECTION } from './plugin'; export { LICENSES, REQUIRED_LICENSES, REQUIRED_ROLES } from './security'; export { TABLE_CONFIG } from './table'; export const BASE_PATH = '/management/ingest/beats_management'; diff --git a/x-pack/plugins/beats_management/common/constants/plugin.ts b/x-pack/plugins/beats_management/common/constants/plugin.ts index 87b600b975fe..912bc75b98f6 100644 --- a/x-pack/plugins/beats_management/common/constants/plugin.ts +++ b/x-pack/plugins/beats_management/common/constants/plugin.ts @@ -9,3 +9,4 @@ export const PLUGIN = { ID: 'beats_management', }; export const CONFIG_PREFIX = 'xpack.beats'; +export const MANAGEMENT_SECTION = 'beats_management'; diff --git a/x-pack/plugins/beats_management/public/bootstrap.tsx b/x-pack/plugins/beats_management/public/bootstrap.tsx index 04d3eada6112..4a4d3a893286 100644 --- a/x-pack/plugins/beats_management/public/bootstrap.tsx +++ b/x-pack/plugins/beats_management/public/bootstrap.tsx @@ -15,11 +15,18 @@ import { CoreSetup } from '../../../../src/core/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { LicensingPluginSetup } from '../../licensing/public'; import { BeatsManagementConfigType } from '../common'; +import { MANAGEMENT_SECTION } from '../common/constants'; async function startApp(libs: FrontendLibs, core: CoreSetup) { - await libs.framework.waitUntilFrameworkReady(); + const [startServices] = await Promise.all([ + core.getStartServices(), + libs.framework.waitUntilFrameworkReady(), + ]); - if (libs.framework.licenseIsAtLeast('standard')) { + const capabilities = startServices[0].application.capabilities; + const hasBeatsCapability = capabilities.management.ingest?.[MANAGEMENT_SECTION] ?? false; + + if (libs.framework.licenseIsAtLeast('standard') && hasBeatsCapability) { const mount = async (params: any) => { const [coreStart, pluginsStart] = await core.getStartServices(); setServices(coreStart, pluginsStart, params); diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts index f86f9c5eb8c7..03a9a7760849 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -11,6 +11,7 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import { isLeft } from 'fp-ts/lib/Either'; import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; +import { MANAGEMENT_SECTION } from '../../../../common/constants'; import { SecurityPluginSetup } from '../../../../../security/public'; import { BufferedKibanaServiceCall, KibanaAdapterServiceRefs, KibanaUIConfig } from '../../types'; import { @@ -107,7 +108,7 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter { public registerManagementUI(mount: RegisterManagementAppArgs['mount']) { const section = this.management.sections.section.ingest; section.registerApp({ - id: 'beats_management', + id: MANAGEMENT_SECTION, title: i18n.translate('xpack.beatsManagement.centralManagementLinkLabel', { defaultMessage: 'Beats Central Management', }), diff --git a/x-pack/plugins/logstash/public/plugin.ts b/x-pack/plugins/logstash/public/plugin.ts index 8f88f626160c..cfca262ec09c 100644 --- a/x-pack/plugins/logstash/public/plugin.ts +++ b/x-pack/plugins/logstash/public/plugin.ts @@ -6,11 +6,11 @@ */ import { i18n } from '@kbn/i18n'; -import { Subscription } from 'rxjs'; +import { Subscription, Subject, combineLatest } from 'rxjs'; import { map } from 'rxjs/operators'; import { once } from 'lodash'; -import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { Capabilities, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { HomePublicPluginSetup, FeatureCatalogueCategory, @@ -30,6 +30,7 @@ interface SetupDeps { export class LogstashPlugin implements Plugin { private licenseSubscription?: Subscription; + private capabilities$ = new Subject(); public setup(core: CoreSetup, plugins: SetupDeps) { const logstashLicense$ = plugins.licensing.license$.pipe( @@ -51,35 +52,43 @@ export class LogstashPlugin implements Plugin { }, }); - this.licenseSubscription = logstashLicense$.subscribe((license: any) => { - if (license.enableLinks) { - managementApp.enable(); - } else { - managementApp.disable(); - } + this.licenseSubscription = combineLatest([logstashLicense$, this.capabilities$]).subscribe( + ([license, capabilities]) => { + const shouldShow = license.enableLinks && capabilities.management.ingest.pipelines === true; + if (shouldShow) { + managementApp.enable(); + } else { + managementApp.disable(); + } - if (plugins.home && license.enableLinks) { - // Ensure that we don't register the feature more than once - once(() => { - plugins.home!.featureCatalogue.register({ - id: 'management_logstash', - title: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesTitle', { - defaultMessage: 'Logstash Pipelines', - }), - description: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesDescription', { - defaultMessage: 'Create, delete, update, and clone data ingestion pipelines.', - }), - icon: 'pipelineApp', - path: '/app/management/ingest/pipelines', - showOnHomePage: false, - category: FeatureCatalogueCategory.ADMIN, + if (plugins.home && shouldShow) { + // Ensure that we don't register the feature more than once + once(() => { + plugins.home!.featureCatalogue.register({ + id: 'management_logstash', + title: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesTitle', { + defaultMessage: 'Logstash Pipelines', + }), + description: i18n.translate( + 'xpack.logstash.homeFeature.logstashPipelinesDescription', + { + defaultMessage: 'Create, delete, update, and clone data ingestion pipelines.', + } + ), + icon: 'pipelineApp', + path: '/app/management/ingest/pipelines', + showOnHomePage: false, + category: FeatureCatalogueCategory.ADMIN, + }); }); - }); + } } - }); + ); } - public start(core: CoreStart) {} + public start(core: CoreStart) { + this.capabilities$.next(core.application.capabilities); + } public stop() { if (this.licenseSubscription) { From f3debcd084b8a90912987d6d38e03dd46728adf6 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 10 Feb 2021 13:40:21 -0600 Subject: [PATCH 21/32] [Metrics UI] Fix node details overlay title with long host name (#90825) --- .../components/node_details/overlay.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx index a3a47276bb52..e313cf0762af 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx @@ -89,11 +89,11 @@ export const NodeContextPopover = ({ - +

{node.name}

-
+ @@ -194,3 +194,12 @@ const OverlayPanel = euiStyled(EuiPanel).attrs({ paddingSize: 'none' })` max-width: 100%; } `; + +const OverlayTitle = euiStyled(EuiFlexItem)` + overflow: hidden; + & h4 { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } +`; From c2b41c484b7612a9af92893f492f435f414320f6 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Wed, 10 Feb 2021 12:03:34 -0800 Subject: [PATCH 22/32] [Fleet] Revamp integration detail page (#90887) * Extract integration detail page changes from POC * Remove unneccessary link wrappers * Remove unused import * Fix method name * Fix linting --- .../applications/fleet/components/header.tsx | 4 +- .../tutorial_module_notice.tsx | 2 +- .../fleet/constants/page_paths.ts | 15 +- .../fleet/hooks/use_breadcrumbs.tsx | 17 +- .../create_package_policy_page/index.tsx | 2 +- .../edit_package_policy_page/index.tsx | 6 +- .../sections/epm/components/package_card.tsx | 2 +- .../epm/hooks/use_package_install.tsx | 3 +- .../screens/detail/components/icon_panel.tsx | 60 +++++ .../epm/screens/detail/components/index.tsx | 9 + .../integration_agent_policy_count.tsx | 2 +- .../screens/detail/components/update_icon.tsx | 24 ++ .../sections/epm/screens/detail/content.tsx | 114 --------- .../epm/screens/detail/content_collapse.tsx | 98 -------- .../epm/screens/detail/custom/custom.tsx | 37 +++ .../epm/screens/detail/custom/index.ts | 7 + .../epm/screens/detail/index.test.tsx | 14 +- .../sections/epm/screens/detail/index.tsx | 182 ++++++++------ .../sections/epm/screens/detail/layout.tsx | 52 ---- .../epm/screens/detail/overview/details.tsx | 159 ++++++++++++ .../epm/screens/detail/overview/index.ts | 7 + .../{ => overview}/markdown_renderers.tsx | 0 .../epm/screens/detail/overview/overview.tsx | 57 +++++ .../screens/detail/{ => overview}/readme.tsx | 17 +- .../screens/detail/overview/screenshots.tsx | 94 +++++++ .../epm/screens/detail/overview_panel.tsx | 23 -- .../epm/screens/detail/policies/index.ts | 7 + .../package_policies.tsx} | 45 ++-- .../screens/detail/{ => policies}/persona.tsx | 3 +- .../use_package_policies_with_agent_policy.ts | 14 +- .../epm/screens/detail/screenshots.tsx | 92 ------- .../confirm_package_install.tsx | 0 .../confirm_package_uninstall.tsx | 0 .../epm/screens/detail/settings/index.ts | 7 + .../{ => settings}/installation_button.tsx | 6 +- .../epm/screens/detail/settings/settings.tsx | 238 ++++++++++++++++++ .../epm/screens/detail/settings_panel.tsx | 230 ----------------- .../public/applications/fleet/types/index.ts | 2 + .../components/fleet_trusted_apps_card.tsx | 5 +- .../page_objects/fleet_integrations_page.ts | 2 +- 40 files changed, 900 insertions(+), 758 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/icon_panel.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/index.tsx rename x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/{ => components}/integration_agent_policy_count.tsx (90%) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/update_icon.tsx delete mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx delete mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content_collapse.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/custom/custom.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/custom/index.ts delete mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/layout.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/details.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/index.ts rename x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/{ => overview}/markdown_renderers.tsx (100%) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/overview.tsx rename x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/{ => overview}/readme.tsx (82%) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/screenshots.tsx delete mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview_panel.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/index.ts rename x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/{package_policies_panel.tsx => policies/package_policies.tsx} (86%) rename x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/{ => policies}/persona.tsx (99%) rename x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/{ => policies}/use_package_policies_with_agent_policy.ts (87%) delete mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx rename x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/{ => settings}/confirm_package_install.tsx (100%) rename x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/{ => settings}/confirm_package_uninstall.tsx (100%) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/index.ts rename x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/{ => settings}/installation_button.tsx (96%) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/settings.tsx delete mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings_panel.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/header.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/header.tsx index 1b580a528fe0..d12311cf16d3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/header.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/header.tsx @@ -37,7 +37,7 @@ export interface HeaderProps { leftColumn?: JSX.Element; rightColumn?: JSX.Element; rightColumnGrow?: EuiFlexItemProps['grow']; - tabs?: EuiTabProps[]; + tabs?: Array & { name?: JSX.Element | string }>; tabsClassName?: string; 'data-test-subj'?: string; } @@ -73,7 +73,7 @@ export const Header: React.FC = ({ {tabs.map((props) => ( - + {props.name} ))} diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx index 069b34e13c9a..88b6933fe477 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/home_integration/tutorial_module_notice.tsx @@ -43,7 +43,7 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName } ), availableAsIntegrationLink: ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts index da0e40270053..bcb450d5ec94 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts @@ -18,7 +18,10 @@ export type StaticPage = | 'data_streams'; export type DynamicPage = - | 'integration_details' + | 'integration_details_overview' + | 'integration_details_policies' + | 'integration_details_settings' + | 'integration_details_custom' | 'integration_policy_edit' | 'policy_details' | 'add_integration_from_policy' @@ -43,6 +46,10 @@ export const PAGE_ROUTING_PATHS = { integrations_all: '/integrations', integrations_installed: '/integrations/installed', integration_details: '/integrations/detail/:pkgkey/:panel?', + integration_details_overview: '/integrations/detail/:pkgkey/overview', + integration_details_policies: '/integrations/detail/:pkgkey/policies', + integration_details_settings: '/integrations/detail/:pkgkey/settings', + integration_details_custom: '/integrations/detail/:pkgkey/custom', integration_policy_edit: '/integrations/edit-integration/:packagePolicyId', policies: '/policies', policies_list: '/policies', @@ -70,8 +77,10 @@ export const pagePathGetters: { integrations: () => '/integrations', integrations_all: () => '/integrations', integrations_installed: () => '/integrations/installed', - integration_details: ({ pkgkey, panel }) => - `/integrations/detail/${pkgkey}${panel ? `/${panel}` : ''}`, + integration_details_overview: ({ pkgkey }) => `/integrations/detail/${pkgkey}/overview`, + integration_details_policies: ({ pkgkey }) => `/integrations/detail/${pkgkey}/policies`, + integration_details_settings: ({ pkgkey }) => `/integrations/detail/${pkgkey}/settings`, + integration_details_custom: ({ pkgkey }) => `/integrations/detail/${pkgkey}/custom`, integration_policy_edit: ({ packagePolicyId }) => `/integrations/edit-integration/${packagePolicyId}`, policies: () => '/policies', diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx index aa869cd076e7..22dfe2e8be51 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx @@ -18,7 +18,7 @@ const BASE_BREADCRUMB: ChromeBreadcrumb = { }; const breadcrumbGetters: { - [key in Page]: (values: DynamicPagePathValues) => ChromeBreadcrumb[]; + [key in Page]?: (values: DynamicPagePathValues) => ChromeBreadcrumb[]; } = { base: () => [BASE_BREADCRUMB], overview: () => [ @@ -65,7 +65,7 @@ const breadcrumbGetters: { }), }, ], - integration_details: ({ pkgTitle }) => [ + integration_details_overview: ({ pkgTitle }) => [ BASE_BREADCRUMB, { href: pagePathGetters.integrations(), @@ -84,7 +84,7 @@ const breadcrumbGetters: { }), }, { - href: pagePathGetters.integration_details({ pkgkey, panel: 'policies' }), + href: pagePathGetters.integration_details_policies({ pkgkey }), text: pkgTitle, }, { text: policyName }, @@ -142,7 +142,7 @@ const breadcrumbGetters: { }), }, { - href: pagePathGetters.integration_details({ pkgkey }), + href: pagePathGetters.integration_details_overview({ pkgkey }), text: pkgTitle, }, { @@ -221,10 +221,11 @@ const breadcrumbGetters: { export function useBreadcrumbs(page: Page, values: DynamicPagePathValues = {}) { const { chrome, http } = useStartServices(); - const breadcrumbs: ChromeBreadcrumb[] = breadcrumbGetters[page](values).map((breadcrumb) => ({ - ...breadcrumb, - href: breadcrumb.href ? http.basePath.prepend(`${BASE_PATH}#${breadcrumb.href}`) : undefined, - })); + const breadcrumbs: ChromeBreadcrumb[] = + breadcrumbGetters[page]?.(values).map((breadcrumb) => ({ + ...breadcrumb, + href: breadcrumb.href ? http.basePath.prepend(`${BASE_PATH}#${breadcrumb.href}`) : undefined, + })) || []; const docTitle: string[] = [...breadcrumbs] .reverse() .map((breadcrumb) => breadcrumb.text as string); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index a8a11c583535..a07601210042 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -217,7 +217,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { } return from === 'policy' ? getHref('policy_details', { policyId: agentPolicyId || policyId }) - : getHref('integration_details', { pkgkey }); + : getHref('integration_details_overview', { pkgkey }); }, [agentPolicyId, policyId, from, getHref, pkgkey, routeState]); const cancelClickHandler: ReactEventHandler = useCallback( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 94133ecdef33..b0f2232cf506 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -246,9 +246,8 @@ export const EditPackagePolicyForm = memo<{ const cancelUrl = useMemo((): string => { if (packageInfo && policyId) { return from === 'package-edit' - ? getHref('integration_details', { + ? getHref('integration_details_policies', { pkgkey: pkgKeyFromPackageInfo(packageInfo!), - panel: 'policies', }) : getHref('policy_details', { policyId }); } @@ -258,9 +257,8 @@ export const EditPackagePolicyForm = memo<{ const successRedirectPath = useMemo(() => { if (packageInfo && policyId) { return from === 'package-edit' - ? getPath('integration_details', { + ? getPath('integration_details_policies', { pkgkey: pkgKeyFromPackageInfo(packageInfo!), - panel: 'policies', }) : getPath('policy_details', { policyId }); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_card.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_card.tsx index 9c1a2d2fad99..cac8ff7d5e7a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_card.tsx @@ -43,7 +43,7 @@ export function PackageCard({ title={title || ''} description={description} icon={} - href={getHref('integration_details', { pkgkey: `${name}-${urlVersion}` })} + href={getHref('integration_details_overview', { pkgkey: `${name}-${urlVersion}` })} betaBadgeLabel={release && release !== 'ga' ? RELEASE_BADGE_LABEL[release] : undefined} betaBadgeTooltipContent={ release && release !== 'ga' ? RELEASE_BADGE_DESCRIPTION[release] : undefined diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_package_install.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_package_install.tsx index e5e7f9f81fd1..d3fccb600173 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_package_install.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_package_install.tsx @@ -90,9 +90,8 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar } else { setPackageInstallStatus({ name, status: InstallStatus.installed, version }); if (fromUpdate) { - const settingsPath = getPath('integration_details', { + const settingsPath = getPath('integration_details_settings', { pkgkey: `${name}-${version}`, - panel: 'settings', }); history.push(settingsPath); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/icon_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/icon_panel.tsx new file mode 100644 index 000000000000..b2495b607af5 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/icon_panel.tsx @@ -0,0 +1,60 @@ +/* + * 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 styled from 'styled-components'; +import { EuiIcon, EuiPanel } from '@elastic/eui'; +import { usePackageIconType, UsePackageIconType } from '../../../../../hooks'; +import { Loading } from '../../../../../components'; + +const PanelWrapper = styled.div` + // NOTE: changes to the width here will impact navigation tabs page layout under integration package details + width: ${(props) => + parseFloat(props.theme.eui.euiSize) * 6 + parseFloat(props.theme.eui.euiSizeXL) * 2}px; + height: 1px; + z-index: 1; +`; + +const Panel = styled(EuiPanel)` + padding: ${(props) => props.theme.eui.spacerSizes.xl}; + margin-bottom: -100%; + svg, + img { + height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px; + width: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px; + } + .euiFlexItem { + height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px; + justify-content: center; + } +`; + +export function IconPanel({ + packageName, + version, + icons, +}: Pick) { + const iconType = usePackageIconType({ packageName, version, icons }); + + return ( + + + + + + ); +} + +export function LoadingIconPanel() { + return ( + + + + + + ); +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/index.tsx new file mode 100644 index 000000000000..8424fecad08c --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { UpdateIcon } from './update_icon'; +export { IntegrationAgentPolicyCount } from './integration_agent_policy_count'; +export { IconPanel, LoadingIconPanel } from './icon_panel'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/integration_agent_policy_count.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/integration_agent_policy_count.tsx similarity index 90% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/integration_agent_policy_count.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/integration_agent_policy_count.tsx index 27d0a19aba5c..eeb74526046e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/integration_agent_policy_count.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/integration_agent_policy_count.tsx @@ -6,7 +6,7 @@ */ import React, { memo } from 'react'; -import { useGetPackageStats } from '../../../../hooks'; +import { useGetPackageStats } from '../../../../../hooks'; /** * Displays a count of Agent Policies that are using the given integration diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/update_icon.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/update_icon.tsx new file mode 100644 index 000000000000..d7ad6667b6db --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/update_icon.tsx @@ -0,0 +1,24 @@ +/* + * 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 { EuiIconTip, EuiIconProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const UpdateIcon = ({ size = 'm' }: { size?: EuiIconProps['size'] }) => ( + +); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx deleted file mode 100644 index e6c5a1278949..000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content.tsx +++ /dev/null @@ -1,114 +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 { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import React, { memo, useMemo } from 'react'; -import styled from 'styled-components'; -import { Redirect } from 'react-router-dom'; -import { DetailParams } from '.'; -import { DetailViewPanelName, PackageInfo } from '../../../../types'; -import { AssetsFacetGroup } from '../../components/assets_facet_group'; -import { CenterColumn, LeftColumn, RightColumn } from './layout'; -import { OverviewPanel } from './overview_panel'; -import { PackagePoliciesPanel } from './package_policies_panel'; -import { SettingsPanel } from './settings_panel'; -import { useUIExtension } from '../../../../hooks/use_ui_extension'; -import { ExtensionWrapper } from '../../../../components/extension_wrapper'; -import { useLink } from '../../../../hooks'; -import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info'; - -type ContentProps = PackageInfo & Pick; - -const LeftSideColumn = styled(LeftColumn)` - /* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */ - &&& { - margin-top: 77px; - } -`; - -// fixes IE11 problem with nested flex items -const ContentFlexGroup = styled(EuiFlexGroup)` - flex: 0 0 auto !important; -`; - -export function Content(props: ContentProps) { - const { panel } = props; - const showRightColumn = useMemo(() => { - const fullWidthContentPages: DetailViewPanelName[] = ['policies', 'custom']; - return !fullWidthContentPages.includes(panel!); - }, [panel]); - - return ( - - - - - - {showRightColumn && ( - - - - )} - - ); -} - -interface ContentPanelProps { - packageInfo: PackageInfo; - panel: DetailViewPanelName; -} -export const ContentPanel = memo(({ panel, packageInfo }) => { - const { name, version, assets, title, removable, latestVersion } = packageInfo; - const pkgkey = pkgKeyFromPackageInfo(packageInfo); - - const CustomView = useUIExtension(name, 'package-detail-custom'); - const { getPath } = useLink(); - - switch (panel) { - case 'settings': - return ( - - ); - case 'policies': - return ; - case 'custom': - return CustomView ? ( - - - - ) : ( - - ); - case 'overview': - default: - return ; - } -}); - -type RightColumnContentProps = PackageInfo & Pick; -function RightColumnContent(props: RightColumnContentProps) { - const { assets, panel } = props; - switch (panel) { - case 'overview': - return assets ? ( - - - - - - ) : null; - default: - return ; - } -} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content_collapse.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content_collapse.tsx deleted file mode 100644 index c6b578dd5364..000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/content_collapse.tsx +++ /dev/null @@ -1,98 +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 { EuiButton, EuiButtonEmpty, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import React, { Fragment, useCallback, useLayoutEffect, useRef, useState } from 'react'; -import styled from 'styled-components'; - -const BottomFade = styled.div` - width: 100%; - background: ${(props) => - `linear-gradient(${props.theme.eui.euiColorEmptyShade}00 0%, ${props.theme.eui.euiColorEmptyShade} 100%)`}; - margin-top: -${(props) => parseInt(props.theme.eui.spacerSizes.xl, 10) * 2}px; - height: ${(props) => parseInt(props.theme.eui.spacerSizes.xl, 10) * 2}px; - position: absolute; -`; -const ContentCollapseContainer = styled.div` - position: relative; -`; -const CollapseButtonContainer = styled.div` - display: inline-block; - background-color: ${(props) => props.theme.eui.euiColorEmptyShade}; - position: absolute; - left: 50%; - transform: translateX(-50%); - top: ${(props) => parseInt(props.theme.eui.euiButtonHeight, 10) / 2}px; -`; -const CollapseButtonTop = styled(EuiButtonEmpty)` - float: right; -`; - -const CollapseButton = ({ - open, - toggleCollapse, -}: { - open: boolean; - toggleCollapse: () => void; -}) => { - return ( -
- - - - - {open ? 'Collapse' : 'Read more'} - - -
- ); -}; - -export const ContentCollapse = ({ children }: { children: React.ReactNode }) => { - const [open, setOpen] = useState(false); - const [height, setHeight] = useState('auto'); - const [collapsible, setCollapsible] = useState(true); - const contentEl = useRef(null); - const collapsedHeight = 360; - - // if content is too small, don't collapse - useLayoutEffect( - () => - contentEl.current && contentEl.current.clientHeight < collapsedHeight - ? setCollapsible(false) - : setHeight(collapsedHeight), - [] - ); - - const clickOpen = useCallback(() => { - setOpen(!open); - }, [open]); - - return ( - - {collapsible ? ( - -
- {open && ( - - Collapse - - )} - {children} -
- {!open && } - -
- ) : ( -
{children}
- )} -
- ); -}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/custom/custom.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/custom/custom.tsx new file mode 100644 index 000000000000..f005c1e24dee --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/custom/custom.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { memo, useMemo } from 'react'; +import { Redirect } from 'react-router-dom'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useUIExtension } from '../../../../../hooks/use_ui_extension'; +import { useLink } from '../../../../../hooks'; +import { PackageInfo } from '../../../../../types'; +import { pkgKeyFromPackageInfo } from '../../../../../services/pkg_key_from_package_info'; +import { ExtensionWrapper } from '../../../../../components/extension_wrapper'; + +interface Props { + packageInfo: PackageInfo; +} + +export const CustomViewPage: React.FC = memo(({ packageInfo }) => { + const CustomView = useUIExtension(packageInfo.name, 'package-detail-custom'); + const { getPath } = useLink(); + const pkgkey = useMemo(() => pkgKeyFromPackageInfo(packageInfo), [packageInfo]); + + return CustomView ? ( + + + + + + + + + ) : ( + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/custom/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/custom/index.ts new file mode 100644 index 000000000000..bc480b1daa35 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/custom/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { CustomViewPage } from './custom'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx index b60d3b5eb1f2..32e39d7c4d6e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx @@ -28,7 +28,7 @@ import { act, cleanup } from '@testing-library/react'; describe('when on integration detail', () => { const pkgkey = 'nginx-0.3.7'; - const detailPageUrlPath = pagePathGetters.integration_details({ pkgkey }); + const detailPageUrlPath = pagePathGetters.integration_details_overview({ pkgkey }); let testRenderer: TestRenderer; let renderResult: ReturnType; let mockedApi: MockedApi; @@ -100,7 +100,7 @@ describe('when on integration detail', () => { it('should redirect if custom url is accessed', () => { act(() => { testRenderer.history.push( - pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7', panel: 'custom' }) + pagePathGetters.integration_details_custom({ pkgkey: 'nginx-0.3.7' }) ); }); expect(testRenderer.history.location.pathname).toEqual(detailPageUrlPath); @@ -148,7 +148,7 @@ describe('when on integration detail', () => { it('should display custom content when tab is clicked', async () => { act(() => { testRenderer.history.push( - pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7', panel: 'custom' }) + pagePathGetters.integration_details_custom({ pkgkey: 'nginx-0.3.7' }) ); }); await lazyComponentWasRendered; @@ -173,14 +173,14 @@ describe('when on integration detail', () => { onCancelNavigateTo: [ 'fleet', { - path: '#/integrations/detail/nginx-0.3.7', + path: '#/integrations/detail/nginx-0.3.7/overview', }, ], - onCancelUrl: '#/integrations/detail/nginx-0.3.7', + onCancelUrl: '#/integrations/detail/nginx-0.3.7/overview', onSaveNavigateTo: [ 'fleet', { - path: '#/integrations/detail/nginx-0.3.7', + path: '#/integrations/detail/nginx-0.3.7/overview', }, ], }); @@ -188,7 +188,7 @@ describe('when on integration detail', () => { }); describe('and on the Policies Tab', () => { - const policiesTabURLPath = pagePathGetters.integration_details({ pkgkey, panel: 'policies' }); + const policiesTabURLPath = pagePathGetters.integration_details_policies({ pkgkey }); beforeEach(() => { testRenderer.history.push(policiesTabURLPath); render(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx index fabcd2ab917a..3cb57b63e707 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx @@ -4,72 +4,50 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import React, { useEffect, useState, useMemo, useCallback, ReactEventHandler } from 'react'; -import { useHistory, useLocation, useParams } from 'react-router-dom'; +import React, { ReactEventHandler, useCallback, useEffect, useMemo, useState } from 'react'; +import { Redirect, Route, Switch, useHistory, useLocation, useParams } from 'react-router-dom'; import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiText, - EuiSpacer, EuiBetaBadge, EuiButton, + EuiButtonEmpty, EuiDescriptionList, - EuiDescriptionListTitle, EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useUIExtension } from '../../../../hooks/use_ui_extension'; +import { PAGE_ROUTING_PATHS, PLUGIN_ID } from '../../../../constants'; +import { useCapabilities, useGetPackageInfoByKey, useLink } from '../../../../hooks'; +import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info'; import { CreatePackagePolicyRouteState, DetailViewPanelName, - entries, InstallStatus, PackageInfo, } from '../../../../types'; -import { Loading, Error } from '../../../../components'; -import { - useGetPackageInfoByKey, - useBreadcrumbs, - useLink, - useCapabilities, -} from '../../../../hooks'; +import { Error, Loading } from '../../../../components'; +import { useBreadcrumbs } from '../../../../hooks'; import { WithHeaderLayout, WithHeaderLayoutProps } from '../../../../layouts'; +import { RELEASE_BADGE_DESCRIPTION, RELEASE_BADGE_LABEL } from '../../components/release_badge'; import { useSetPackageInstallStatus } from '../../hooks'; -import { IconPanel, LoadingIconPanel } from '../../components/icon_panel'; -import { RELEASE_BADGE_LABEL, RELEASE_BADGE_DESCRIPTION } from '../../components/release_badge'; -import { UpdateIcon } from '../../components/icons'; -import { Content } from './content'; +import { IntegrationAgentPolicyCount, UpdateIcon, IconPanel, LoadingIconPanel } from './components'; +import { OverviewPage } from './overview'; +import { PackagePoliciesPage } from './policies'; +import { SettingsPage } from './settings'; +import { CustomViewPage } from './custom'; import './index.scss'; -import { useUIExtension } from '../../../../hooks/use_ui_extension'; -import { PLUGIN_ID } from '../../../../../../../common/constants'; -import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info'; -import { IntegrationAgentPolicyCount } from './integration_agent_policy_count'; - -export const DEFAULT_PANEL: DetailViewPanelName = 'overview'; export interface DetailParams { pkgkey: string; panel?: DetailViewPanelName; } -const PanelDisplayNames: Record = { - overview: i18n.translate('xpack.fleet.epm.packageDetailsNav.overviewLinkText', { - defaultMessage: 'Overview', - }), - policies: i18n.translate('xpack.fleet.epm.packageDetailsNav.packagePoliciesLinkText', { - defaultMessage: 'Policies', - }), - settings: i18n.translate('xpack.fleet.epm.packageDetailsNav.settingsLinkText', { - defaultMessage: 'Settings', - }), - custom: i18n.translate('xpack.fleet.epm.packageDetailsNav.packageCustomLinkText', { - defaultMessage: 'Advanced', - }), -}; - const Divider = styled.div` width: 0; height: 100%; @@ -82,12 +60,12 @@ const FlexItemWithMinWidth = styled(EuiFlexItem)` `; function Breadcrumbs({ packageTitle }: { packageTitle: string }) { - useBreadcrumbs('integration_details', { pkgTitle: packageTitle }); + useBreadcrumbs('integration_details_overview', { pkgTitle: packageTitle }); return null; } export function Detail() { - const { pkgkey, panel = DEFAULT_PANEL } = useParams(); + const { pkgkey, panel } = useParams(); const { getHref, getPath } = useLink(); const hasWriteCapabilites = useCapabilities().write; const history = useHistory(); @@ -247,7 +225,7 @@ export function Detail() { { isDivider: true }, { label: i18n.translate('xpack.fleet.epm.usedByLabel', { - defaultMessage: 'Agent Policies', + defaultMessage: 'Agent policies', }), 'data-test-subj': 'agentPolicyCount', content: , @@ -306,37 +284,79 @@ export function Detail() { ] ); - const tabs = useMemo(() => { + const headerTabs = useMemo(() => { if (!packageInfo) { return []; } + const packageInfoKey = pkgKeyFromPackageInfo(packageInfo); - return (entries(PanelDisplayNames) - .filter(([panelId]) => { - // Don't show `Policies` tab if package is not installed - if (panelId === 'policies' && packageInstallStatus !== InstallStatus.installed) { - return false; - } + const tabs: WithHeaderLayoutProps['tabs'] = [ + { + id: 'overview', + name: ( + + ), + isSelected: panel === 'overview', + 'data-test-subj': `tab-overview`, + href: getHref('integration_details_overview', { + pkgkey: packageInfoKey, + }), + }, + ]; - // Don't show `custom` tab if a custom component is not registered - if (panelId === 'custom' && !showCustomTab) { - return false; - } + if (packageInstallStatus === InstallStatus.installed) { + tabs.push({ + id: 'policies', + name: ( + + ), + isSelected: panel === 'policies', + 'data-test-subj': `tab-policies`, + href: getHref('integration_details_policies', { + pkgkey: packageInfoKey, + }), + }); + } + + tabs.push({ + id: 'settings', + name: ( + + ), + isSelected: panel === 'settings', + 'data-test-subj': `tab-settings`, + href: getHref('integration_details_settings', { + pkgkey: packageInfoKey, + }), + }); + + if (showCustomTab) { + tabs.push({ + id: 'custom', + name: ( + + ), + isSelected: panel === 'custom', + 'data-test-subj': `tab-custom`, + href: getHref('integration_details_custom', { + pkgkey: packageInfoKey, + }), + }); + } - return true; - }) - .map(([panelId, display]) => { - return { - id: panelId, - name: display, - isSelected: panelId === panel, - 'data-test-subj': `tab-${panelId}`, - href: getHref('integration_details', { - pkgkey: pkgKeyFromPackageInfo(packageInfo || {}), - panel: panelId, - }), - }; - }) as unknown) as WithHeaderLayoutProps['tabs']; + return tabs; }, [getHref, packageInfo, panel, showCustomTab, packageInstallStatus]); return ( @@ -344,7 +364,7 @@ export function Detail() { leftColumn={headerLeftContent} rightColumn={headerRightContent} rightColumnGrow={false} - tabs={tabs} + tabs={headerTabs} tabsClassName="fleet__epm__shiftNavTabs" > {packageInfo ? : null} @@ -361,7 +381,21 @@ export function Detail() { ) : isLoading || !packageInfo ? ( ) : ( - + + + + + + + + + + + + + + + )} ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/layout.tsx deleted file mode 100644 index f751a4c057b7..000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/layout.tsx +++ /dev/null @@ -1,52 +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 { EuiFlexItem } from '@elastic/eui'; -import React, { FunctionComponent, ReactNode } from 'react'; -import { FlexItemGrowSize } from '@elastic/eui/src/components/flex/flex_item'; - -interface ColumnProps { - children?: ReactNode; - className?: string; - columnGrow?: FlexItemGrowSize; -} - -export const LeftColumn: FunctionComponent = ({ - columnGrow = 2, - children, - ...rest -}) => { - return ( - - {children} - - ); -}; - -export const CenterColumn: FunctionComponent = ({ - columnGrow = 9, - children, - ...rest -}) => { - return ( - - {children} - - ); -}; - -export const RightColumn: FunctionComponent = ({ - columnGrow = 3, - children, - ...rest -}) => { - return ( - - {children} - - ); -}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/details.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/details.tsx new file mode 100644 index 000000000000..3b9daaeb0216 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/details.tsx @@ -0,0 +1,159 @@ +/* + * 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, { memo, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiTextColor, + EuiDescriptionList, + EuiNotificationBadge, +} from '@elastic/eui'; +import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; +import { + PackageInfo, + PackageSpecCategory, + AssetTypeToParts, + KibanaAssetType, + entries, +} from '../../../../../types'; +import { useGetCategories } from '../../../../../hooks'; +import { AssetTitleMap, DisplayedAssets, ServiceTitleMap } from '../../../constants'; + +interface Props { + packageInfo: PackageInfo; +} + +export const Details: React.FC = memo(({ packageInfo }) => { + const { data: categoriesData, isLoading: isLoadingCategories } = useGetCategories(); + const packageCategories: string[] = useMemo(() => { + if (!isLoadingCategories && categoriesData && categoriesData.response) { + return categoriesData.response + .filter((category) => packageInfo.categories?.includes(category.id as PackageSpecCategory)) + .map((category) => category.title); + } + return []; + }, [categoriesData, isLoadingCategories, packageInfo.categories]); + + const listItems = useMemo(() => { + // Base details: version and categories + const items: EuiDescriptionListProps['listItems'] = [ + { + title: ( + + + + ), + description: packageInfo.version, + }, + { + title: ( + + + + ), + description: packageCategories.join(', '), + }, + ]; + + // Asset details and counts + entries(packageInfo.assets).forEach(([service, typeToParts]) => { + // Filter out assets we are not going to display + // (currently we only display Kibana and Elasticsearch assets) + const filteredTypes: AssetTypeToParts = entries(typeToParts).reduce( + (acc: any, [asset, value]) => { + if (DisplayedAssets[service].includes(asset)) acc[asset] = value; + return acc; + }, + {} + ); + if (Object.entries(filteredTypes).length) { + items.push({ + title: ( + + + + ), + description: ( + + {entries(filteredTypes).map(([_type, parts]) => { + const type = _type as KibanaAssetType; + return ( + + + {AssetTitleMap[type]} + + {parts.length} + + + + ); + })} + + ), + }); + } + }); + + // Feature (data stream type) details + const dataStreamTypes = [ + ...new Set(packageInfo.data_streams?.map((dataStream) => dataStream.type) || []), + ]; + if (dataStreamTypes.length) { + items.push({ + title: ( + + + + ), + description: dataStreamTypes.join(', '), + }); + } + + // License details + if (packageInfo.license) { + items.push({ + title: ( + + + + ), + description: packageInfo.license, + }); + } + + return items; + }, [ + packageCategories, + packageInfo.assets, + packageInfo.data_streams, + packageInfo.license, + packageInfo.version, + ]); + + return ( + + + +

+ +

+
+
+ + + +
+ ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/index.ts new file mode 100644 index 000000000000..70a5453aeb42 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { OverviewPage } from './overview'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/markdown_renderers.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/markdown_renderers.tsx similarity index 100% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/markdown_renderers.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/markdown_renderers.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/overview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/overview.tsx new file mode 100644 index 000000000000..4e45ecd2f70c --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/overview.tsx @@ -0,0 +1,57 @@ +/* + * 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, { memo } from 'react'; +import styled from 'styled-components'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { PackageInfo } from '../../../../../types'; +import { Screenshots } from './screenshots'; +import { Readme } from './readme'; +import { Details } from './details'; + +interface Props { + packageInfo: PackageInfo; +} + +const LeftColumn = styled(EuiFlexItem)` + /* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */ + &&& { + margin-top: 77px; + } +`; + +export const OverviewPage: React.FC = memo(({ packageInfo }: Props) => { + return ( + + + + {packageInfo.readme ? ( + + ) : null} + + + + {packageInfo.screenshots && packageInfo.screenshots.length ? ( + + + + ) : null} + +
+ + + + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/readme.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/readme.tsx similarity index 82% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/readme.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/readme.tsx index 55050894d870..92022f5e97a6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/readme.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/readme.tsx @@ -8,10 +8,9 @@ import { EuiLoadingContent, EuiText } from '@elastic/eui'; import React, { Fragment, useEffect, useState } from 'react'; import ReactMarkdown from 'react-markdown'; -import { useLinks } from '../../hooks'; -import { ContentCollapse } from './content_collapse'; +import { useLinks } from '../../../hooks'; +import { sendGetFileByPath } from '../../../../../hooks'; import { markdownRenderers } from './markdown_renderers'; -import { sendGetFileByPath } from '../../../../hooks'; export function Readme({ readmePath, @@ -43,13 +42,11 @@ export function Readme({ return ( {markdown !== undefined ? ( - - - + ) : ( {/* simulates a long page of text loading */} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/screenshots.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/screenshots.tsx new file mode 100644 index 000000000000..33b2c3efe2fb --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/screenshots.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState, useMemo, memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiText, EuiPagination } from '@elastic/eui'; +import { ScreenshotItem } from '../../../../../types'; +import { useLinks } from '../../../hooks'; + +interface ScreenshotProps { + images: ScreenshotItem[]; + packageName: string; + version: string; +} + +export const Screenshots: React.FC = memo(({ images, packageName, version }) => { + const { toPackageImage } = useLinks(); + const [currentImageIndex, setCurrentImageIndex] = useState(0); + const maxImageIndex = useMemo(() => images.length - 1, [images.length]); + const currentImageUrl = useMemo( + () => toPackageImage(images[currentImageIndex], packageName, version), + [currentImageIndex, images, packageName, toPackageImage, version] + ); + + return ( + + {/* Title with carousel navigation */} + + + + +

+ +

+
+
+ + setCurrentImageIndex(activePage)} + compressed + /> + +
+
+ + {/* Current screenshot */} + + {currentImageUrl ? ( + + ) : ( + + )} + +
+ ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview_panel.tsx deleted file mode 100644 index 750f8ad3f80b..000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview_panel.tsx +++ /dev/null @@ -1,23 +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 { EuiSpacer } from '@elastic/eui'; -import React, { Fragment } from 'react'; -import { PackageInfo } from '../../../../types'; -import { Readme } from './readme'; -import { Screenshots } from './screenshots'; - -export function OverviewPanel(props: PackageInfo) { - const { screenshots, readme, name, version } = props; - return ( - - {readme && } - - {screenshots && } - - ); -} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/index.ts new file mode 100644 index 000000000000..d92dfc923632 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { PackagePoliciesPage } from './package_policies'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/package_policies.tsx similarity index 86% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/package_policies.tsx index b8e5388dc153..53cd642a1ef7 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/package_policies_panel.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/package_policies.tsx @@ -12,21 +12,22 @@ import { EuiBasicTable, EuiLink, EuiTableFieldDataColumnType, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react'; -import { useGetPackageInstallStatus } from '../../hooks'; -import { InstallStatus } from '../../../../types'; -import { useLink } from '../../../../hooks'; -import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../../common/constants'; -import { useUrlPagination } from '../../../../hooks'; +import { InstallStatus } from '../../../../../types'; +import { useLink, useUrlPagination } from '../../../../../hooks'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; +import { LinkAndRevision, LinkAndRevisionProps } from '../../../../../components'; +import { LinkedAgentCount } from '../../../../../components/linked_agent_count'; +import { useGetPackageInstallStatus } from '../../../hooks'; import { PackagePolicyAndAgentPolicy, usePackagePoliciesWithAgentPolicy, } from './use_package_policies_with_agent_policy'; -import { LinkAndRevision, LinkAndRevisionProps } from '../../../../components'; import { Persona } from './persona'; -import { LinkedAgentCount } from '../../../../components/linked_agent_count'; const IntegrationDetailsLink = memo<{ packagePolicy: PackagePolicyAndAgentPolicy['packagePolicy']; @@ -52,6 +53,7 @@ const AgentPolicyDetailLink = memo<{ children: ReactNode; }>(({ agentPolicyId, revision, children }) => { const { getHref } = useLink(); + return ( { +export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps) => { const { getPath } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); @@ -197,18 +199,25 @@ export const PackagePoliciesPanel = ({ name, version }: PackagePoliciesPanelProp // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab if (packageInstallStatus.status !== InstallStatus.installed) { - return ; + return ( + + ); } return ( - + + + + + + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/persona.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/persona.tsx similarity index 99% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/persona.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/persona.tsx index 373140f6fef6..02e36df57009 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/persona.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/persona.tsx @@ -4,9 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { EuiAvatar, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import React, { CSSProperties, memo, useCallback } from 'react'; +import { EuiAvatar, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { EuiAvatarProps } from '@elastic/eui/src/components/avatar/avatar'; const MIN_WIDTH: CSSProperties = { minWidth: 0 }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/use_package_policies_with_agent_policy.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts similarity index 87% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/use_package_policies_with_agent_policy.ts rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts index 4f014d93c5d5..ff8343314b4a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/use_package_policies_with_agent_policy.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts @@ -6,19 +6,19 @@ */ import { useEffect, useMemo, useState } from 'react'; -import { PackagePolicy } from '../../../../../../../common/types/models'; import { + PackagePolicy, GetAgentPoliciesResponse, GetAgentPoliciesResponseItem, -} from '../../../../../../../common/types/rest_spec'; -import { useGetPackagePolicies } from '../../../../hooks/use_request'; + GetPackagePoliciesResponse, +} from '../../../../../types'; +import { agentPolicyRouteService } from '../../../../../services'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; +import { useGetPackagePolicies } from '../../../../../hooks'; import { SendConditionalRequestConfig, useConditionalRequest, -} from '../../../../hooks/use_request/use_request'; -import { agentPolicyRouteService } from '../../../../../../../common/services'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../../common/constants'; -import { GetPackagePoliciesResponse } from '../../../../../../../common/types/rest_spec'; +} from '../../../../../hooks/use_request/use_request'; export interface PackagePolicyEnriched extends PackagePolicy { _agentPolicy: GetAgentPoliciesResponseItem | undefined; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx deleted file mode 100644 index d1e6cf07f57a..000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx +++ /dev/null @@ -1,92 +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, { Fragment } from 'react'; -import styled from 'styled-components'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; -import { ScreenshotItem } from '../../../../types'; -import { useLinks } from '../../hooks'; - -interface ScreenshotProps { - images: ScreenshotItem[]; - packageName: string; - version: string; -} - -const getHorizontalPadding = (styledProps: any): number => - parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 2; -const getVerticalPadding = (styledProps: any): number => - parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 1.75; -const getPadding = (styledProps: any) => - styledProps.hascaption - ? `${styledProps.theme.eui.paddingSizes.xl} ${getHorizontalPadding( - styledProps - )}px ${getVerticalPadding(styledProps)}px` - : `${getHorizontalPadding(styledProps)}px ${getVerticalPadding(styledProps)}px`; -const ScreenshotsContainer = styled(EuiFlexGroup)` - background: linear-gradient(360deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%), - ${(styledProps) => styledProps.theme.eui.euiColorPrimary}; - padding: ${(styledProps) => getPadding(styledProps)}; - flex: 0 0 auto; - border-radius: ${(styledProps) => styledProps.theme.eui.euiBorderRadius}; -`; - -// fixes ie11 problems with nested flex items -const NestedEuiFlexItem = styled(EuiFlexItem)` - flex: 0 0 auto !important; -`; - -export function Screenshots(props: ScreenshotProps) { - const { toPackageImage } = useLinks(); - const { images, packageName, version } = props; - - // for now, just get first image - const image = images[0]; - const hasCaption = image.title ? true : false; - const screenshotUrl = toPackageImage(image, packageName, version); - - return ( - - -

- -

-
- - - {hasCaption && ( - - - {image.title} - - - - )} - {screenshotUrl && ( - - {/* By default EuiImage sets width to 100% and Figure to 22.5rem for size=l images, - set image to same width. Will need to update if size changes. - */} - - - )} - -
- ); -} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/confirm_package_install.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_install.tsx similarity index 100% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/confirm_package_install.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_install.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/confirm_package_uninstall.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_uninstall.tsx similarity index 100% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/confirm_package_uninstall.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_uninstall.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/index.ts new file mode 100644 index 000000000000..33c6a11f4067 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { SettingsPage } from './settings'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/installation_button.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/installation_button.tsx similarity index 96% rename from x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/installation_button.tsx rename to x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/installation_button.tsx index ae85f59424fb..ca37095f5db1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/installation_button.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/installation_button.tsx @@ -8,9 +8,9 @@ import { EuiButton } from '@elastic/eui'; import React, { Fragment, useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { PackageInfo, InstallStatus } from '../../../../types'; -import { useCapabilities } from '../../../../hooks'; -import { useUninstallPackage, useGetPackageInstallStatus, useInstallPackage } from '../../hooks'; +import { PackageInfo, InstallStatus } from '../../../../../types'; +import { useCapabilities } from '../../../../../hooks'; +import { useUninstallPackage, useGetPackageInstallStatus, useInstallPackage } from '../../../hooks'; import { ConfirmPackageUninstall } from './confirm_package_uninstall'; import { ConfirmPackageInstall } from './confirm_package_install'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/settings.tsx new file mode 100644 index 000000000000..2c3559a65130 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/settings.tsx @@ -0,0 +1,238 @@ +/* + * 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, { memo } from 'react'; +import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui'; +import { InstallStatus, PackageInfo } from '../../../../../types'; +import { useGetPackagePolicies } from '../../../../../hooks'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; +import { useGetPackageInstallStatus } from '../../../hooks'; +import { UpdateIcon } from '../components'; +import { InstallationButton } from './installation_button'; + +const SettingsTitleCell = styled.td` + padding-right: ${(props) => props.theme.eui.spacerSizes.xl}; + padding-bottom: ${(props) => props.theme.eui.spacerSizes.m}; +`; + +const UpdatesAvailableMsgContainer = styled.span` + padding-left: ${(props) => props.theme.eui.spacerSizes.s}; +`; + +const NoteLabel = () => ( + +); +const UpdatesAvailableMsg = () => ( + + + + +); + +interface Props { + packageInfo: PackageInfo; +} + +export const SettingsPage: React.FC = memo(({ packageInfo }: Props) => { + const { name, title, removable, latestVersion, version } = packageInfo; + const getPackageInstallStatus = useGetPackageInstallStatus(); + const { data: packagePoliciesData } = useGetPackagePolicies({ + perPage: 0, + page: 1, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${name}`, + }); + const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name); + const packageHasUsages = !!packagePoliciesData?.total; + const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; + const isViewingOldPackage = version < latestVersion; + // hide install/remove options if the user has version of the package is installed + // and this package is out of date or if they do have a version installed but it's not this one + const hideInstallOptions = + (installationStatus === InstallStatus.notInstalled && isViewingOldPackage) || + (installationStatus === InstallStatus.installed && installedVersion !== version); + + const isUpdating = installationStatus === InstallStatus.installing && installedVersion; + return ( + + + + + +

+ +

+
+ + {installedVersion !== null && ( +
+ +

+ +

+
+ + + + + + + + + + + + + + + + +
+ + {installedVersion} + + {updateAvailable && } +
+ + {latestVersion} + +
+ {updateAvailable && ( +

+ +

+ )} +
+ )} + {!hideInstallOptions && !isUpdating && ( +
+ + {installationStatus === InstallStatus.notInstalled || + installationStatus === InstallStatus.installing ? ( +
+ +

+ +

+
+ +

+ +

+
+ ) : ( +
+ +

+ +

+
+ +

+ +

+
+ )} + + +

+ +

+
+
+ {packageHasUsages && removable === true && ( +

+ + + + ), + }} + /> +

+ )} + {removable === false && ( +

+ + + + ), + }} + /> +

+ )} +
+ )} +
+
+
+ ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings_panel.tsx deleted file mode 100644 index b9835f41a0d7..000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings_panel.tsx +++ /dev/null @@ -1,230 +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 } from '@kbn/i18n/react'; -import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { EuiSpacer } from '@elastic/eui'; -import styled from 'styled-components'; -import { InstallStatus, PackageInfo } from '../../../../types'; -import { useGetPackagePolicies } from '../../../../hooks'; -import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../constants'; -import { useGetPackageInstallStatus } from '../../hooks'; -import { InstallationButton } from './installation_button'; -import { UpdateIcon } from '../../components/icons'; - -const SettingsTitleCell = styled.td` - padding-right: ${(props) => props.theme.eui.spacerSizes.xl}; - padding-bottom: ${(props) => props.theme.eui.spacerSizes.m}; -`; - -const UpdatesAvailableMsgContainer = styled.span` - padding-left: ${(props) => props.theme.eui.spacerSizes.s}; -`; - -const NoteLabel = () => ( - -); -const UpdatesAvailableMsg = () => ( - - - - -); - -export const SettingsPanel = ( - props: Pick -) => { - const { name, title, removable, latestVersion, version } = props; - const getPackageInstallStatus = useGetPackageInstallStatus(); - const { data: packagePoliciesData } = useGetPackagePolicies({ - perPage: 0, - page: 1, - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${props.name}`, - }); - const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name); - const packageHasUsages = !!packagePoliciesData?.total; - const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; - const isViewingOldPackage = version < latestVersion; - // hide install/remove options if the user has version of the package is installed - // and this package is out of date or if they do have a version installed but it's not this one - const hideInstallOptions = - (installationStatus === InstallStatus.notInstalled && isViewingOldPackage) || - (installationStatus === InstallStatus.installed && installedVersion !== version); - - const isUpdating = installationStatus === InstallStatus.installing && installedVersion; - return ( - - -

- -

-
- - {installedVersion !== null && ( -
- -

- -

-
- - - - - - - - - - - - - - - - -
- - {installedVersion} - - {updateAvailable && } -
- - {latestVersion} - -
- {updateAvailable && ( -

- -

- )} -
- )} - {!hideInstallOptions && !isUpdating && ( -
- - {installationStatus === InstallStatus.notInstalled || - installationStatus === InstallStatus.installing ? ( -
- -

- -

-
- -

- -

-
- ) : ( -
- -

- -

-
- -

- -

-
- )} - - -

- -

-
-
- {packageHasUsages && removable === true && ( -

- - - - ), - }} - /> -

- )} - {removable === false && ( -

- - - - ), - }} - /> -

- )} -
- )} -
- ); -}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts index 5ccbfc953428..89aa5ad1add3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts @@ -49,6 +49,7 @@ export { CreatePackagePolicyResponse, UpdatePackagePolicyRequest, UpdatePackagePolicyResponse, + GetPackagePoliciesResponse, // API schemas - Data streams GetDataStreamsResponse, // API schemas - Agents @@ -122,6 +123,7 @@ export { InstallationStatus, Installable, RegistryRelease, + PackageSpecCategory, } from '../../../../common'; export * from './intra_app_route_state'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx index 13da6ac90403..fe6f82e632f7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx @@ -32,10 +32,7 @@ export const FleetTrustedAppsCard = memo(( const trustedAppsListUrlPath = getTrustedAppsListPath(); const trustedAppRouteState = useMemo(() => { - const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details({ - pkgkey, - panel: 'custom', - })}`; + const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details_custom({ pkgkey })}`; return { backButtonLabel: i18n.translate( 'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel', diff --git a/x-pack/test/security_solution_endpoint/page_objects/fleet_integrations_page.ts b/x-pack/test/security_solution_endpoint/page_objects/fleet_integrations_page.ts index 15e1c37befd9..791fed942496 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/fleet_integrations_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/fleet_integrations_page.ts @@ -18,7 +18,7 @@ export function FleetIntegrations({ getService, getPageObjects }: FtrProviderCon return { async navigateToIntegrationDetails(pkgkey: string) { await pageObjects.common.navigateToApp(PLUGIN_ID, { - hash: pagePathGetters.integration_details({ pkgkey }), + hash: pagePathGetters.integration_details_overview({ pkgkey }), }); }, From 4c878be613c8af221f5d5f1da6f6c5cac119a845 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Wed, 10 Feb 2021 23:05:46 +0300 Subject: [PATCH 23/32] Bump "monaco-editor" and "react-monaco-editor" to latest versions (#90158) * Update version of react-monaco-editor and monaco-editor libraries * Fix yarn lock file * Fix CI * Fix unit tests * Fix CI * Fix comment * move monaco instance in window.MonacoEnvironment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 4 ++-- packages/kbn-monaco/src/register_globals.ts | 2 ++ packages/kbn-test/jest-preset.js | 4 ++-- packages/kbn-ui-shared-deps/webpack.config.js | 7 +++++++ .../code_editor/code_editor.stories.tsx | 1 - .../public/code_editor/code_editor.test.tsx | 5 ----- .../components/timelion_expression_input.tsx | 1 - test/functional/services/inspector.ts | 2 +- .../expression_input/expression_input.tsx | 1 - yarn.lock | 20 +++++++++---------- 10 files changed, 24 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index aac576dbc356..0fa8ef31ab25 100644 --- a/package.json +++ b/package.json @@ -247,7 +247,7 @@ "moment": "^2.24.0", "moment-duration-format": "^2.3.2", "moment-timezone": "^0.5.27", - "monaco-editor": "^0.17.0", + "monaco-editor": "^0.22.3", "mustache": "^2.3.2", "ngreact": "^0.5.1", "nock": "12.0.3", @@ -772,7 +772,7 @@ "react-fast-compare": "^2.0.4", "react-grid-layout": "^0.16.2", "react-markdown": "^4.3.1", - "react-monaco-editor": "^0.27.0", + "react-monaco-editor": "^0.41.2", "react-popper-tooltip": "^2.10.1", "react-resize-detector": "^4.2.0", "react-reverse-portal": "^1.0.4", diff --git a/packages/kbn-monaco/src/register_globals.ts b/packages/kbn-monaco/src/register_globals.ts index 66a00fff089b..a07d979e2022 100644 --- a/packages/kbn-monaco/src/register_globals.ts +++ b/packages/kbn-monaco/src/register_globals.ts @@ -39,6 +39,8 @@ const mapLanguageIdToWorker: { [key: string]: any } = { // @ts-ignore window.MonacoEnvironment = { + // needed for functional tests so that we can get value from 'editor' + monaco, getWorker: (module: string, languageId: string) => { const workerSrc = mapLanguageIdToWorker[languageId] || defaultWorkerSrc; diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 717be8f413b4..79fc3db86e06 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -93,9 +93,9 @@ module.exports = { // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation transformIgnorePatterns: [ - // ignore all node_modules except monaco-editor which requires babel transforms to handle dynamic import() + // ignore all node_modules except monaco-editor and react-monaco-editor which requires babel transforms to handle dynamic import() // since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842) - '[/\\\\]node_modules(?![\\/\\\\]monaco-editor)[/\\\\].+\\.js$', + '[/\\\\]node_modules(?![\\/\\\\](monaco-editor|react-monaco-editor))[/\\\\].+\\.js$', 'packages/kbn-pm/dist/index.js', ], diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index d04f6e30f316..7ff5978e1f2e 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -85,6 +85,13 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ }, ], }, + { + test: /\.(ttf)(\?|$)/, + loader: 'url-loader', + options: { + limit: 8192, + }, + }, ], }, diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx index 57fcdef86179..a5fdfe773a2f 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.stories.tsx @@ -165,7 +165,6 @@ storiesOf('CodeEditor', module) provideCompletionItems: provideSuggestions, }} options={{ - wordBasedSuggestions: false, quickSuggestions: true, }} /> diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx index c3d465d4f09e..33f0f311d3a4 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.test.tsx @@ -50,9 +50,6 @@ test('editor mount setup', () => { suggestions: [], }), }; - const signatureProvider = { - provideSignatureHelp: () => ({ signatures: [], activeParameter: 0, activeSignature: 0 }), - }; const hoverProvider = { provideHover: (model: monaco.editor.ITextModel, position: monaco.Position) => ({ contents: [], @@ -82,7 +79,6 @@ test('editor mount setup', () => { onChange={() => {}} editorWillMount={editorWillMount} suggestionProvider={suggestionProvider} - signatureProvider={signatureProvider} hoverProvider={hoverProvider} /> ); @@ -99,6 +95,5 @@ test('editor mount setup', () => { // Verify our language features have been registered expect((monaco.languages.onLanguage as jest.Mock).mock.calls.length).toBe(1); expect((monaco.languages.registerCompletionItemProvider as jest.Mock).mock.calls.length).toBe(1); - expect((monaco.languages.registerSignatureHelpProvider as jest.Mock).mock.calls.length).toBe(1); expect((monaco.languages.registerHoverProvider as jest.Mock).mock.calls.length).toBe(1); }); diff --git a/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx b/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx index 2cb244a4d270..d518d9718d5e 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_expression_input.tsx @@ -115,7 +115,6 @@ function TimelionExpressionInput({ value, setValue }: TimelionExpressionInputPro minimap: { enabled: false, }, - wordBasedSuggestions: false, wordWrap: 'on', wrappingIndent: 'indent', }} diff --git a/test/functional/services/inspector.ts b/test/functional/services/inspector.ts index fc4f20504da5..4dc248116ccf 100644 --- a/test/functional/services/inspector.ts +++ b/test/functional/services/inspector.ts @@ -241,7 +241,7 @@ export function InspectorProvider({ getService }: FtrProviderContext) { await retry.try(async () => { request = await browser.execute( - () => (window as any).monaco.editor.getModels()[0].getValue() as string + () => (window as any).MonacoEnvironment.monaco.editor.getModels()[0].getValue() as string ); }); diff --git a/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx b/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx index 27d1f76c2bf8..53ba7996eb64 100644 --- a/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx +++ b/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx @@ -324,7 +324,6 @@ export class ExpressionInput extends React.Component { minimap: { enabled: false, }, - wordBasedSuggestions: false, wordWrap: 'on', wrappingIndent: 'indent', }} diff --git a/yarn.lock b/yarn.lock index c9f3186ffcba..319025b3aab7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6488,7 +6488,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.8.23", "@types/react@^16.9.36": +"@types/react@*", "@types/react@^16.9.36": version "16.9.36" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.36.tgz#ade589ff51e2a903e34ee4669e05dbfa0c1ce849" integrity sha512-mGgUb/Rk/vGx4NCvquRuSH0GHBQKb1OqpGS9cT9lFxlTLHZgkksgI60TuIxubmn7JuCb+sENHhQciqa0npm0AQ== @@ -21069,10 +21069,10 @@ moment-timezone@^0.5.27: resolved "https://registry.yarnpkg.com/moment/-/moment-2.28.0.tgz#cdfe73ce01327cee6537b0fafac2e0f21a237d75" integrity sha512-Z5KOjYmnHyd/ukynmFd/WwyXHd7L4J9vTI/nn5Ap9AVUgaAE15VvQ9MOGmJJygEUklupqIrFnor/tjTwRU+tQw== -monaco-editor@^0.17.0: - version "0.17.1" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.17.1.tgz#8fbe96ca54bfa75262706e044f8f780e904aa45c" - integrity sha512-JAc0mtW7NeO+0SwPRcdkfDbWLgkqL9WfP1NbpP9wNASsW6oWqgZqNIWt4teymGjZIXTElx3dnQmUYHmVrJ7HxA== +monaco-editor@*, monaco-editor@^0.22.3: + version "0.22.3" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.22.3.tgz#69b42451d3116c6c08d9b8e052007ff891fd85d7" + integrity sha512-RM559z2CJbczZ3k2b+ouacMINkAYWwRit4/vs0g2X/lkYefDiu0k2GmgWjAuiIpQi+AqASPOKvXNmYc8KUSvVQ== monitor-event-loop-delay@^1.0.0: version "1.0.0" @@ -24247,12 +24247,12 @@ react-moment-proptypes@^1.7.0: dependencies: moment ">=1.6.0" -react-monaco-editor@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.27.0.tgz#2dbf47b8fd4d8e4763934051f07291d9b128bb89" - integrity sha512-Im40xO4DuFlQ6kVcSBHC+p70fD/5aErUy1uyLT9RZ4nlehn6BOPpwmcw/2IN/LfMvy8X4WmLuuvrNftBZLH+vA== +react-monaco-editor@^0.41.2: + version "0.41.2" + resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.41.2.tgz#7ec9cadc101d73003a908fca61c50011f237d2b5" + integrity sha512-0nNqkkSLtUQDHtcCASv3ccYukD+P2uvFzcFZGh6iWg9RZF3Rj9/+jqsTNo2cl4avkX8JVGC/qnZr/g7hxXTBTQ== dependencies: - "@types/react" "^16.8.23" + monaco-editor "*" prop-types "^15.7.2" react-motion@^0.4.8: From d0900f844df7020ffeeb4aa4acf26ffa40f15eab Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 10 Feb 2021 21:18:41 +0100 Subject: [PATCH 24/32] Limit cardinality of transaction.name (#90955) --- src/optimize/bundles_route/bundles_route.ts | 3 +++ .../server/task_running/task_runner.ts | 17 +++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/optimize/bundles_route/bundles_route.ts b/src/optimize/bundles_route/bundles_route.ts index 6debf4b47659..b88ca7e5c22b 100644 --- a/src/optimize/bundles_route/bundles_route.ts +++ b/src/optimize/bundles_route/bundles_route.ts @@ -10,6 +10,7 @@ import { extname, join } from 'path'; import Hapi from '@hapi/hapi'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; +import agent from 'elastic-apm-node'; import { createDynamicAssetResponse } from './dynamic_asset_response'; import { FileHashCache } from './file_hash_cache'; @@ -101,6 +102,8 @@ function buildRouteForBundles({ method(request: Hapi.Request, h: Hapi.ResponseToolkit) { const ext = extname(request.params.path); + agent.setTransactionName('GET ?/bundles/?'); + if (ext !== '.js' && ext !== '.css') { return h.continue; } diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.ts index 40213b3743d6..ad5a2e11409e 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.ts @@ -201,10 +201,10 @@ export class TaskManagerRunner implements TaskRunner { }); const stopTaskTimer = startTaskTimer(); - const apmTrans = apm.startTransaction( - `taskManager run ${this.instance.taskType}`, - 'taskManager' - ); + const apmTrans = apm.startTransaction(`taskManager run`, 'taskManager'); + apmTrans?.addLabels({ + taskType: this.taskType, + }); try { this.task = this.definition.createTaskRunner(modifiedContext); const result = await this.task.run(); @@ -232,10 +232,11 @@ export class TaskManagerRunner implements TaskRunner { public async markTaskAsRunning(): Promise { performance.mark('markTaskAsRunning_start'); - const apmTrans = apm.startTransaction( - `taskManager markTaskAsRunning ${this.instance.taskType}`, - 'taskManager' - ); + const apmTrans = apm.startTransaction(`taskManager markTaskAsRunning`, 'taskManager'); + + apmTrans?.addLabels({ + taskType: this.taskType, + }); const now = new Date(); try { From 65a3f16c0fa13308c107e28137ef2e9b075ef6c2 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 10 Feb 2021 14:20:09 -0600 Subject: [PATCH 25/32] [Metrics UI] Fix saving/loading saved views from URL (#90216) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../saved_views/toolbar_control.tsx | 6 +- .../containers/saved_view/saved_view.tsx | 64 +++++++++++++++---- .../infra/public/pages/metrics/index.tsx | 6 +- .../pages/metrics/inventory_view/index.tsx | 6 +- 4 files changed, 60 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx index 09e319b9935d..ade43638deb6 100644 --- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -6,7 +6,7 @@ */ import { EuiFlexGroup } from '@elastic/eui'; -import React, { useCallback, useState, useEffect, useContext } from 'react'; +import React, { useCallback, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -21,7 +21,7 @@ import { SavedViewCreateModal } from './create_modal'; import { SavedViewUpdateModal } from './update_modal'; import { SavedViewManageViewsFlyout } from './manage_views_flyout'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { SavedView } from '../../containers/saved_view/saved_view'; +import { useSavedViewContext } from '../../containers/saved_view/saved_view'; import { SavedViewListModal } from './view_list_modal'; interface Props { @@ -47,7 +47,7 @@ export function SavedViewsToolbarControls(props: Props) { updatedView, currentView, setCurrentView, - } = useContext(SavedView.Context); + } = useSavedViewContext(); const [modalOpen, setModalOpen] = useState(false); const [viewListModalOpen, setViewListModalOpen] = useState(false); const [isInvalid, setIsInvalid] = useState(false); diff --git a/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx b/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx index e867cf800f4b..4c4835cbe4cd 100644 --- a/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx +++ b/x-pack/plugins/infra/public/containers/saved_view/saved_view.tsx @@ -6,9 +6,14 @@ */ import createContainer from 'constate'; +import * as rt from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; import { useCallback, useMemo, useState, useEffect, useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { SimpleSavedObject, SavedObjectAttributes } from 'kibana/public'; +import { useUrlState } from '../../utils/use_url_state'; import { useFindSavedObject } from '../../hooks/use_find_saved_object'; import { useCreateSavedObject } from '../../hooks/use_create_saved_object'; import { useDeleteSavedObject } from '../../hooks/use_delete_saved_object'; @@ -39,6 +44,14 @@ interface Props { shouldLoadDefault: boolean; } +const savedViewUrlStateRT = rt.type({ + viewId: rt.string, +}); +type SavedViewUrlState = rt.TypeOf; +const DEFAULT_SAVED_VIEW_STATE: SavedViewUrlState = { + viewId: '0', +}; + export const useSavedView = (props: Props) => { const { source, @@ -52,6 +65,13 @@ export const useSavedView = (props: Props) => { const { data, loading, find, error: errorOnFind, hasView } = useFindSavedObject< SavedViewSavedObject >(viewType); + const [urlState, setUrlState] = useUrlState({ + defaultState: DEFAULT_SAVED_VIEW_STATE, + decodeUrlState, + encodeUrlState, + urlStateKey: 'savedView', + }); + const [shouldLoadDefault] = useState(props.shouldLoadDefault); const [currentView, setCurrentView] = useState | null>(null); const [loadingDefaultView, setLoadingDefaultView] = useState(null); @@ -212,25 +232,35 @@ export const useSavedView = (props: Props) => { }); }, [setCurrentView, defaultViewId, defaultViewState]); - useEffect(() => { - if (loadingDefaultView || currentView || !shouldLoadDefault) { - return; - } - + const loadDefaultViewIfSet = useCallback(() => { if (defaultViewId !== '0') { loadDefaultView(); } else { setDefault(); setLoadingDefaultView(false); } - }, [ - loadDefaultView, - shouldLoadDefault, - setDefault, - loadingDefaultView, - currentView, - defaultViewId, - ]); + }, [defaultViewId, loadDefaultView, setDefault, setLoadingDefaultView]); + + useEffect(() => { + if (loadingDefaultView || currentView || !shouldLoadDefault) { + return; + } + + loadDefaultViewIfSet(); + }, [loadDefaultViewIfSet, loadingDefaultView, currentView, shouldLoadDefault]); + + useEffect(() => { + if (currentView && urlState.viewId !== currentView.id && data) + setUrlState({ viewId: currentView.id }); + }, [urlState, setUrlState, currentView, defaultViewId, data]); + + useEffect(() => { + if (!currentView && !loading && data) { + const viewToSet = views.find((v) => v.id === urlState.viewId); + if (viewToSet) setCurrentView(viewToSet); + else loadDefaultViewIfSet(); + } + }, [loading, currentView, data, views, setCurrentView, loadDefaultViewIfSet, urlState.viewId]); return { views, @@ -260,3 +290,11 @@ export const useSavedView = (props: Props) => { export const SavedView = createContainer(useSavedView); export const [SavedViewProvider, useSavedViewContext] = SavedView; + +const encodeUrlState = (state: SavedViewUrlState) => { + return savedViewUrlStateRT.encode(state); +}; +const decodeUrlState = (value: unknown) => { + const state = pipe(savedViewUrlStateRT.decode(value), fold(constant(undefined), identity)); + return state; +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 8fd32bda7fbc..240cb778275b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -36,7 +36,7 @@ import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time'; import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters'; import { MetricsAlertDropdown } from '../../alerting/common/components/metrics_alert_dropdown'; -import { SavedView } from '../../containers/saved_view/saved_view'; +import { SavedViewProvider } from '../../containers/saved_view/saved_view'; import { AlertPrefillProvider } from '../../alerting/use_alert_prefill'; import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabilities'; import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout'; @@ -195,7 +195,7 @@ const PageContent = (props: { const { options } = useContext(MetricsExplorerOptionsContainer.Context); return ( - - + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx index 7123c022538e..6b980d33c255 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -23,7 +23,7 @@ import { useTrackPageview } from '../../../../../observability/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { Layout } from './components/layout'; import { useLinkProps } from '../../../hooks/use_link_props'; -import { SavedView } from '../../../containers/saved_view/saved_view'; +import { SavedViewProvider } from '../../../containers/saved_view/saved_view'; import { DEFAULT_WAFFLE_VIEW_STATE } from './hooks/use_waffle_view_state'; import { useWaffleOptionsContext } from './hooks/use_waffle_options'; @@ -64,13 +64,13 @@ export const SnapshotPage = () => { ) : metricIndicesExist ? ( <> - - + ) : hasFailedLoadingSource ? ( From a9e6cff88d775c32dce93d76d7295c977382ccd9 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Wed, 10 Feb 2021 15:35:56 -0500 Subject: [PATCH 26/32] [App Search] Relevance Tuning logic listeners (#89461) --- .../components/relevance_tuning/constants.ts | 26 + .../relevance_tuning_logic.test.ts | 913 +++++++++++++++++- .../relevance_tuning_logic.ts | 377 +++++++- .../components/relevance_tuning/types.ts | 22 +- .../components/relevance_tuning/utils.test.ts | 147 +++ .../components/relevance_tuning/utils.ts | 63 ++ 6 files changed, 1512 insertions(+), 36 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts index 3655c60bde3b..211995b2a7d1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts @@ -11,3 +11,29 @@ export const RELEVANCE_TUNING_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.title', { defaultMessage: 'Relevance Tuning' } ); + +export const UPDATE_SUCCESS_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.updateSuccess', + { + defaultMessage: 'Relevance successfully tuned. The changes will impact your results shortly.', + } +); +export const DELETE_SUCCESS_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.deleteSuccess', + { + defaultMessage: + 'Relevance has been reset to default values. The change will impact your results shortly.', + } +); +export const RESET_CONFIRMATION_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.resetConfirmation', + { + defaultMessage: 'Are you sure you want to restore relevance defaults?', + } +); +export const DELETE_CONFIRMATION_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.deleteConfirmation', + { + defaultMessage: 'Are you sure you want to delete this boost?', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index 7f7bce1b7ba9..194848bcfc86 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -5,12 +5,18 @@ * 2.0. */ -import { LogicMounter } from '../../../__mocks__'; +import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; -import { BoostType } from './types'; +import { nextTick } from '@kbn/test/jest'; + +import { Boost, BoostType } from './types'; import { RelevanceTuningLogic } from './'; +jest.mock('../engine', () => ({ + EngineLogic: { values: { engineName: 'test-engine' } }, +})); + describe('RelevanceTuningLogic', () => { const { mount } = new LogicMounter(RelevanceTuningLogic); @@ -32,13 +38,27 @@ describe('RelevanceTuningLogic', () => { schema, schemaConflicts, }; - const searchResults = [{}, {}]; + const searchResults = [ + { + id: { + raw: '1', + }, + _meta: { + id: '1', + score: 100, + engine: 'my-engine', + }, + }, + ]; const DEFAULT_VALUES = { dataLoading: true, schema: {}, schemaConflicts: {}, - searchSettings: {}, + searchSettings: { + boosts: {}, + search_fields: {}, + }, unsavedChanges: false, filterInputValue: '', query: '', @@ -188,6 +208,873 @@ describe('RelevanceTuningLogic', () => { }); }); }); + + describe('setSearchSettingsResponse', () => { + it('should set searchSettings state and unsavedChanges to false', () => { + mount({ + unsavedChanges: true, + }); + RelevanceTuningLogic.actions.setSearchSettingsResponse(searchSettings); + + expect(RelevanceTuningLogic.values).toEqual({ + ...DEFAULT_VALUES, + searchSettings, + unsavedChanges: false, + }); + }); + }); + }); + + describe('listeners', () => { + const { http } = mockHttpValues; + const { flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + let scrollToSpy: jest.SpyInstance; + let confirmSpy: jest.SpyInstance; + + const searchSettingsWithBoost = (boost: Boost) => ({ + ...searchSettings, + boosts: { + foo: [ + { + factor: 1, + type: 'functional', + }, + boost, + ], + }, + }); + + beforeAll(() => { + scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation(() => true); + confirmSpy = jest.spyOn(window, 'confirm'); + }); + + afterAll(() => { + scrollToSpy.mockRestore(); + confirmSpy.mockRestore(); + }); + + describe('initializeRelevanceTuning', () => { + it('should make an API call and set state based on the normalized response', async () => { + mount(); + http.get.mockReturnValueOnce( + Promise.resolve({ + ...relevanceTuningProps, + searchSettings: { + ...relevanceTuningProps.searchSettings, + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + value: 5, + }, + ], + }, + }, + }) + ); + jest.spyOn(RelevanceTuningLogic.actions, 'onInitializeRelevanceTuning'); + + RelevanceTuningLogic.actions.initializeRelevanceTuning(); + await nextTick(); + + expect(http.get).toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/search_settings/details' + ); + expect(RelevanceTuningLogic.actions.onInitializeRelevanceTuning).toHaveBeenCalledWith({ + ...relevanceTuningProps, + searchSettings: { + ...relevanceTuningProps.searchSettings, + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + value: ['5'], + }, + ], + }, + }, + }); + }); + + it('handles errors', async () => { + mount(); + http.get.mockReturnValueOnce(Promise.reject('error')); + + RelevanceTuningLogic.actions.initializeRelevanceTuning(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + + describe('getSearchResults', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should make an API call and set state based on the response', async () => { + const searchSettingsWithNewBoostProp = { + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + newBoost: true, // This should be deleted before sent to the server + }, + ], + }, + }; + + const searchSettingsWithoutNewBoostProp = { + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + }, + ], + }, + }; + + mount({ + query: 'foo', + searchSettings: searchSettingsWithNewBoostProp, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchResults'); + jest.spyOn(RelevanceTuningLogic.actions, 'setResultsLoading'); + http.post.mockReturnValueOnce( + Promise.resolve({ + results: searchResults, + }) + ); + + RelevanceTuningLogic.actions.getSearchResults(); + jest.runAllTimers(); + await nextTick(); + + expect(RelevanceTuningLogic.actions.setResultsLoading).toHaveBeenCalledWith(true); + expect(http.post).toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/search_settings_search', + { + body: JSON.stringify(searchSettingsWithoutNewBoostProp), + query: { + query: 'foo', + }, + } + ); + expect(RelevanceTuningLogic.actions.setSearchResults).toHaveBeenCalledWith(searchResults); + }); + + it("won't send boosts or search_fields on the API call if there are none", async () => { + mount({ + query: 'foo', + searchSettings: { + searchField: {}, + boosts: {}, + }, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchResults'); + http.post.mockReturnValueOnce( + Promise.resolve({ + results: searchResults, + }) + ); + + RelevanceTuningLogic.actions.getSearchResults(); + + jest.runAllTimers(); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/search_settings_search', + { + body: '{}', + query: { + query: 'foo', + }, + } + ); + }); + + it('will call clearSearchResults if there is no query', async () => { + mount({ + query: '', + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchResults'); + jest.spyOn(RelevanceTuningLogic.actions, 'setResultsLoading'); + jest.spyOn(RelevanceTuningLogic.actions, 'clearSearchResults'); + + RelevanceTuningLogic.actions.getSearchResults(); + jest.runAllTimers(); + await nextTick(); + + expect(RelevanceTuningLogic.actions.clearSearchResults).toHaveBeenCalled(); + expect(RelevanceTuningLogic.actions.setSearchResults).not.toHaveBeenCalled(); + expect(RelevanceTuningLogic.actions.setResultsLoading).not.toHaveBeenCalled(); + }); + + it('handles errors', async () => { + mount({ + query: 'foo', + }); + http.post.mockReturnValueOnce(Promise.reject('error')); + RelevanceTuningLogic.actions.getSearchResults(); + + jest.runAllTimers(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + + describe('setSearchSettings', () => { + it('updates search results whenever search settings are changed', () => { + mount(); + jest.spyOn(RelevanceTuningLogic.actions, 'getSearchResults'); + + RelevanceTuningLogic.actions.setSearchSettings(searchSettings); + + expect(RelevanceTuningLogic.actions.getSearchResults).toHaveBeenCalled(); + }); + }); + + describe('onSearchSettingsSuccess', () => { + it('should save the response, trigger a new search, and then scroll to the top', () => { + mount(); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettingsResponse'); + jest.spyOn(RelevanceTuningLogic.actions, 'getSearchResults'); + + RelevanceTuningLogic.actions.onSearchSettingsSuccess(searchSettings); + + expect(RelevanceTuningLogic.actions.setSearchSettingsResponse).toHaveBeenCalledWith( + searchSettings + ); + expect(RelevanceTuningLogic.actions.getSearchResults).toHaveBeenCalled(); + expect(scrollToSpy).toHaveBeenCalledWith(0, 0); + }); + }); + + describe('onSearchSettingsError', () => { + it('scrolls to the top', () => { + mount(); + RelevanceTuningLogic.actions.onSearchSettingsError(); + expect(scrollToSpy).toHaveBeenCalledWith(0, 0); + }); + }); + + describe('updateSearchSettings', () => { + it('calls an API endpoint and handles success response', async () => { + const searchSettingsWithNewBoostProp = { + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + newBoost: true, // This should be deleted before sent to the server + }, + ], + }, + }; + + const searchSettingsWithoutNewBoostProp = { + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + }, + ], + }, + }; + mount({ + searchSettings: searchSettingsWithNewBoostProp, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'onSearchSettingsSuccess'); + http.put.mockReturnValueOnce(Promise.resolve(searchSettingsWithoutNewBoostProp)); + + RelevanceTuningLogic.actions.updateSearchSettings(); + await nextTick(); + + expect(http.put).toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/search_settings', + { + body: JSON.stringify(searchSettingsWithoutNewBoostProp), + } + ); + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Relevance successfully tuned. The changes will impact your results shortly.' + ); + expect(RelevanceTuningLogic.actions.onSearchSettingsSuccess).toHaveBeenCalledWith( + searchSettingsWithoutNewBoostProp + ); + }); + + it('handles errors', async () => { + mount(); + jest.spyOn(RelevanceTuningLogic.actions, 'onSearchSettingsError'); + http.put.mockReturnValueOnce(Promise.reject('error')); + + RelevanceTuningLogic.actions.updateSearchSettings(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + expect(RelevanceTuningLogic.actions.onSearchSettingsError).toHaveBeenCalled(); + }); + }); + + describe('resetSearchSettings', () => { + it('calls and API endpoint, shows a success message, and saves the response', async () => { + mount(); + jest.spyOn(RelevanceTuningLogic.actions, 'onSearchSettingsSuccess'); + confirmSpy.mockImplementation(() => true); + http.post.mockReturnValueOnce(Promise.resolve(searchSettings)); + + RelevanceTuningLogic.actions.resetSearchSettings(); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/search_settings/reset' + ); + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Relevance has been reset to default values. The change will impact your results shortly.' + ); + expect(RelevanceTuningLogic.actions.onSearchSettingsSuccess).toHaveBeenCalledWith( + searchSettings + ); + }); + + it('does nothing if the user does not confirm', async () => { + mount(); + confirmSpy.mockImplementation(() => false); + + RelevanceTuningLogic.actions.resetSearchSettings(); + + expect(http.post).not.toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/search_settings/reset' + ); + }); + + it('handles errors', async () => { + mount(); + jest.spyOn(RelevanceTuningLogic.actions, 'onSearchSettingsError'); + confirmSpy.mockImplementation(() => true); + http.post.mockReturnValueOnce(Promise.reject('error')); + + RelevanceTuningLogic.actions.resetSearchSettings(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + expect(RelevanceTuningLogic.actions.onSearchSettingsError).toHaveBeenCalled(); + }); + }); + + describe('toggleSearchField', () => { + it('updates search weight to 1 in search fields when enabling', () => { + mount({ + searchSettings: { + ...searchSettings, + search_fields: { + bar: { + weight: 1, + }, + }, + }, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.toggleSearchField('foo', false); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + search_fields: { + bar: { + weight: 1, + }, + foo: { + weight: 1, + }, + }, + }); + }); + + it('removes fields from search fields when disabling', () => { + mount({ + searchSettings: { + ...searchSettings, + search_fields: { + bar: { + weight: 1, + }, + }, + }, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.toggleSearchField('bar', true); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + }); + }); + }); + + describe('updateFieldWeight', () => { + it('updates the search weight in search fields', () => { + mount({ + searchSettings, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateFieldWeight('foo', 3); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + search_fields: { + foo: { + weight: 3, + }, + }, + }); + }); + + it('will round decimal numbers', () => { + mount({ + searchSettings, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateFieldWeight('foo', 3.9393939); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + search_fields: { + foo: { + weight: 3.9, + }, + }, + }); + }); + }); + + describe('addBoost', () => { + it('adds a boost of given type for the given field', () => { + mount({ + searchSettings: { + ...searchSettings, + boosts: { + foo: [ + { + factor: 2, + type: 'value', + }, + ], + }, + }, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.addBoost('foo', 'functional'); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + boosts: { + foo: [ + { + factor: 2, + type: 'value', + }, + { + factor: 1, + newBoost: true, + type: 'functional', + }, + ], + }, + }); + }); + + it('works even if there are no boosts yet', () => { + mount({ + searchSettings: { + ...searchSettings, + boosts: {}, + }, + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.addBoost('foo', 'functional'); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + boosts: { + foo: [ + { + factor: 1, + newBoost: true, + type: 'functional', + }, + ], + }, + }); + }); + }); + + describe('deleteBoost', () => { + it('deletes the boost with the given name and index', () => { + mount({ + searchSettings: { + ...searchSettings, + boosts: { + foo: [ + { + factor: 1, + type: 'functional', + }, + { + factor: 2, + type: 'value', + }, + ], + }, + }, + }); + confirmSpy.mockImplementation(() => true); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.deleteBoost('foo', 1); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + boosts: { + foo: [ + { + factor: 1, + type: 'functional', + }, + ], + }, + }); + }); + + it('will delete they field key in boosts if this is the last boost or that field', () => { + mount({ + searchSettings: { + ...searchSettings, + boosts: { + foo: [ + { + factor: 1, + type: 'functional', + }, + ], + }, + }, + }); + confirmSpy.mockImplementation(() => true); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.deleteBoost('foo', 0); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ + ...searchSettings, + boosts: {}, + }); + }); + + it('will do nothing if the user does not confirm', () => { + mount({ + searchSettings: { + ...searchSettings, + boosts: { + foo: [ + { + factor: 1, + type: 'functional', + }, + ], + }, + }, + }); + confirmSpy.mockImplementation(() => false); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.deleteBoost('foo', 0); + + expect(RelevanceTuningLogic.actions.setSearchSettings).not.toHaveBeenCalled(); + }); + }); + + describe('updateBoostFactor', () => { + it('updates the boost factor of the target boost', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateBoostFactor('foo', 1, 5); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 5, + type: 'functional', + }) + ); + }); + + it('will round decimal numbers', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateBoostFactor('foo', 1, 5.293191); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 5.3, + type: 'functional', + }) + ); + }); + }); + + describe('updateBoostValue', () => { + it('will update the boost value and update search reuslts', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a', 'b', 'c'], + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateBoostValue('foo', 1, 1, 'a'); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a', 'a', 'c'], + }) + ); + }); + + it('will create a new array if no array exists yet for value', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateBoostValue('foo', 1, 0, 'a'); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a'], + }) + ); + }); + }); + + describe('updateBoostCenter', () => { + it('will parse the provided provided value and set the center to that parsed value', () => { + mount({ + schema: { + foo: 'number', + }, + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'proximity', + center: 1, + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateBoostCenter('foo', 1, '4'); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'proximity', + center: 4, + }) + ); + }); + }); + + describe('addBoostValue', () => { + it('will add an empty boost value', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a'], + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.addBoostValue('foo', 1); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a', ''], + }) + ); + }); + + it('will add two empty boost values if none exist yet', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.addBoostValue('foo', 1); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['', ''], + }) + ); + }); + + it('will still work if the boost index is out of range', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a', ''], + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.addBoostValue('foo', 10); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a', ''], + }) + ); + }); + }); + + describe('removeBoostValue', () => { + it('will remove a boost value', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a', 'b', 'c'], + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.removeBoostValue('foo', 1, 1); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + value: ['a', 'c'], + }) + ); + }); + + it('will do nothing if boost values do not exist', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.removeBoostValue('foo', 1, 1); + + expect(RelevanceTuningLogic.actions.setSearchSettings).not.toHaveBeenCalled(); + }); + }); + + describe('updateBoostSelectOption', () => { + it('will update the boost', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateBoostSelectOption('foo', 1, 'function', 'exponential'); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + function: 'exponential', + }) + ); + }); + + it('can also update operation', () => { + mount({ + searchSettings: searchSettingsWithBoost({ + factor: 1, + type: 'functional', + }), + }); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); + + RelevanceTuningLogic.actions.updateBoostSelectOption('foo', 1, 'operation', 'add'); + + expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( + searchSettingsWithBoost({ + factor: 1, + type: 'functional', + operation: 'add', + }) + ); + }); + }); + + describe('updateSearchValue', () => { + it('should update the query then update search results', () => { + mount(); + jest.spyOn(RelevanceTuningLogic.actions, 'setSearchQuery'); + jest.spyOn(RelevanceTuningLogic.actions, 'getSearchResults'); + + RelevanceTuningLogic.actions.updateSearchValue('foo'); + + expect(RelevanceTuningLogic.actions.setSearchQuery).toHaveBeenCalledWith('foo'); + expect(RelevanceTuningLogic.actions.getSearchResults).toHaveBeenCalled(); + }); + }); }); describe('selectors', () => { @@ -253,24 +1140,6 @@ describe('RelevanceTuningLogic', () => { }); expect(RelevanceTuningLogic.values.filteredSchemaFields).toEqual(['bar', 'baz']); }); - - it('should return all schema fields if there is no filter applied', () => { - mount({ - filterTerm: '', - schema: { - id: 'string', - foo: 'string', - bar: 'string', - baz: 'string', - }, - }); - expect(RelevanceTuningLogic.values.filteredSchemaFields).toEqual([ - 'id', - 'foo', - 'bar', - 'baz', - ]); - }); }); describe('filteredSchemaFieldsWithConflicts', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts index d4ec5e37f6ce..cd3d8b5686cc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts @@ -6,10 +6,28 @@ */ import { kea, MakeLogicType } from 'kea'; +import { omit, cloneDeep, isEmpty } from 'lodash'; +import { setSuccessMessage, flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; import { Schema, SchemaConflicts } from '../../../shared/types'; -import { SearchSettings } from './types'; +import { EngineLogic } from '../engine'; +import { Result } from '../result/types'; + +import { + UPDATE_SUCCESS_MESSAGE, + RESET_CONFIRMATION_MESSAGE, + DELETE_SUCCESS_MESSAGE, + DELETE_CONFIRMATION_MESSAGE, +} from './constants'; +import { BaseBoost, Boost, BoostType, SearchSettings } from './types'; +import { + filterIfTerm, + parseBoostCenter, + removeBoostStateProps, + normalizeBoostValues, +} from './utils'; interface RelevanceTuningProps { searchSettings: SearchSettings; @@ -22,15 +40,60 @@ interface RelevanceTuningActions { setSearchSettings(searchSettings: SearchSettings): { searchSettings: SearchSettings }; setFilterValue(value: string): string; setSearchQuery(value: string): string; - setSearchResults(searchResults: object[]): object[]; + setSearchResults(searchResults: Result[]): Result[]; setResultsLoading(resultsLoading: boolean): boolean; clearSearchResults(): void; resetSearchSettingsState(): void; dismissSchemaConflictCallout(): void; + initializeRelevanceTuning(): void; + getSearchResults(): void; + setSearchSettingsResponse(searchSettings: SearchSettings): { searchSettings: SearchSettings }; + onSearchSettingsSuccess(searchSettings: SearchSettings): { searchSettings: SearchSettings }; + onSearchSettingsError(): void; + updateSearchSettings(): void; + resetSearchSettings(): void; + toggleSearchField(name: string, disableField: boolean): { name: string; disableField: boolean }; + updateFieldWeight(name: string, weight: number): { name: string; weight: number }; + addBoost(name: string, type: BoostType): { name: string; type: BoostType }; + deleteBoost(name: string, index: number): { name: string; index: number }; + updateBoostFactor( + name: string, + index: number, + factor: number + ): { name: string; index: number; factor: number }; + updateBoostValue( + name: string, + boostIndex: number, + valueIndex: number, + value: string + ): { name: string; boostIndex: number; valueIndex: number; value: string }; + updateBoostCenter( + name: string, + boostIndex: number, + value: string | number + ): { name: string; boostIndex: number; value: string | number }; + addBoostValue(name: string, boostIndex: number): { name: string; boostIndex: number }; + removeBoostValue( + name: string, + boostIndex: number, + valueIndex: number + ): { name: string; boostIndex: number; valueIndex: number }; + updateBoostSelectOption( + name: string, + boostIndex: number, + optionType: keyof BaseBoost, + value: string + ): { + name: string; + boostIndex: number; + optionType: keyof BaseBoost; + value: string; + }; + updateSearchValue(query: string): string; } interface RelevanceTuningValues { - searchSettings: Partial; + searchSettings: SearchSettings; schema: Schema; schemaFields: string[]; schemaFieldsWithConflicts: string[]; @@ -43,15 +106,10 @@ interface RelevanceTuningValues { query: string; unsavedChanges: boolean; dataLoading: boolean; - searchResults: object[] | null; + searchResults: Result[] | null; resultsLoading: boolean; } -// If the user hasn't entered a filter, then we can skip filtering the array entirely -const filterIfTerm = (array: string[], filterTerm: string): string[] => { - return filterTerm === '' ? array : array.filter((item) => item.includes(filterTerm)); -}; - export const RelevanceTuningLogic = kea< MakeLogicType >({ @@ -66,13 +124,47 @@ export const RelevanceTuningLogic = kea< clearSearchResults: true, resetSearchSettingsState: true, dismissSchemaConflictCallout: true, + initializeRelevanceTuning: true, + getSearchResults: true, + setSearchSettingsResponse: (searchSettings) => ({ + searchSettings, + }), + onSearchSettingsSuccess: (searchSettings) => ({ searchSettings }), + onSearchSettingsError: () => true, + updateSearchSettings: true, + resetSearchSettings: true, + toggleSearchField: (name, disableField) => ({ name, disableField }), + updateFieldWeight: (name, weight) => ({ name, weight }), + addBoost: (name, type) => ({ name, type }), + deleteBoost: (name, index) => ({ name, index }), + updateBoostFactor: (name, index, factor) => ({ name, index, factor }), + updateBoostValue: (name, boostIndex, valueIndex, value) => ({ + name, + boostIndex, + valueIndex, + value, + }), + updateBoostCenter: (name, boostIndex, value) => ({ name, boostIndex, value }), + addBoostValue: (name, boostIndex) => ({ name, boostIndex }), + removeBoostValue: (name, boostIndex, valueIndex) => ({ name, boostIndex, valueIndex }), + updateBoostSelectOption: (name, boostIndex, optionType, value) => ({ + name, + boostIndex, + optionType, + value, + }), + updateSearchValue: (query) => query, }), reducers: () => ({ searchSettings: [ - {}, + { + search_fields: {}, + boosts: {}, + }, { onInitializeRelevanceTuning: (_, { searchSettings }) => searchSettings, setSearchSettings: (_, { searchSettings }) => searchSettings, + setSearchSettingsResponse: (_, { searchSettings }) => searchSettings, }, ], schema: [ @@ -109,6 +201,7 @@ export const RelevanceTuningLogic = kea< false, { setSearchSettings: () => true, + setSearchSettingsResponse: () => false, }, ], @@ -155,4 +248,268 @@ export const RelevanceTuningLogic = kea< (schema: Schema): boolean => Object.keys(schema).length >= 2, ], }), + listeners: ({ actions, values }) => ({ + initializeRelevanceTuning: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const url = `/api/app_search/engines/${engineName}/search_settings/details`; + + try { + const response = await http.get(url); + actions.onInitializeRelevanceTuning({ + ...response, + searchSettings: { + ...response.searchSettings, + boosts: normalizeBoostValues(response.searchSettings.boosts), + }, + }); + } catch (e) { + flashAPIErrors(e); + } + }, + getSearchResults: async (_, breakpoint) => { + await breakpoint(250); + + const query = values.query; + if (!query) return actions.clearSearchResults(); + + const { engineName } = EngineLogic.values; + const { http } = HttpLogic.values; + const { search_fields: searchFields, boosts } = removeBoostStateProps(values.searchSettings); + const url = `/api/app_search/engines/${engineName}/search_settings_search`; + + actions.setResultsLoading(true); + + try { + const response = await http.post(url, { + query: { + query, + }, + body: JSON.stringify({ + boosts: isEmpty(boosts) ? undefined : boosts, + search_fields: isEmpty(searchFields) ? undefined : searchFields, + }), + }); + + actions.setSearchResults(response.results); + } catch (e) { + flashAPIErrors(e); + } + }, + setSearchSettings: () => { + actions.getSearchResults(); + }, + onSearchSettingsSuccess: ({ searchSettings }) => { + actions.setSearchSettingsResponse(searchSettings); + actions.getSearchResults(); + window.scrollTo(0, 0); + }, + onSearchSettingsError: () => { + window.scrollTo(0, 0); + }, + updateSearchSettings: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const url = `/api/app_search/engines/${engineName}/search_settings`; + + try { + const response = await http.put(url, { + body: JSON.stringify(removeBoostStateProps(values.searchSettings)), + }); + setSuccessMessage(UPDATE_SUCCESS_MESSAGE); + actions.onSearchSettingsSuccess(response); + } catch (e) { + flashAPIErrors(e); + actions.onSearchSettingsError(); + } + }, + resetSearchSettings: async () => { + if (window.confirm(RESET_CONFIRMATION_MESSAGE)) { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const url = `/api/app_search/engines/${engineName}/search_settings/reset`; + + try { + const response = await http.post(url); + setSuccessMessage(DELETE_SUCCESS_MESSAGE); + actions.onSearchSettingsSuccess(response); + } catch (e) { + flashAPIErrors(e); + actions.onSearchSettingsError(); + } + } + }, + toggleSearchField: ({ name, disableField }) => { + const { searchSettings } = values; + + const searchFields = disableField + ? omit(searchSettings.search_fields, name) + : { ...searchSettings.search_fields, [name]: { weight: 1 } }; + + actions.setSearchSettings({ + ...searchSettings, + search_fields: searchFields, + }); + }, + updateFieldWeight: ({ name, weight }) => { + const { searchSettings } = values; + const { search_fields: searchFields } = searchSettings; + + actions.setSearchSettings({ + ...searchSettings, + search_fields: { + ...searchFields, + [name]: { + ...searchFields[name], + weight: Math.round(weight * 10) / 10, + }, + }, + }); + }, + addBoost: ({ name, type }) => { + const { searchSettings } = values; + const { boosts } = searchSettings; + const emptyBoost = { type, factor: 1, newBoost: true }; + let boostArray; + + if (Array.isArray(boosts[name])) { + boostArray = boosts[name].slice(); + boostArray.push(emptyBoost); + } else { + boostArray = [emptyBoost]; + } + + actions.setSearchSettings({ + ...searchSettings, + boosts: { + ...boosts, + [name]: boostArray, + }, + }); + }, + deleteBoost: ({ name, index }) => { + if (window.confirm(DELETE_CONFIRMATION_MESSAGE)) { + const { searchSettings } = values; + const { boosts } = searchSettings; + const boostsRemoved = boosts[name].slice(); + boostsRemoved.splice(index, 1); + const updatedBoosts = { ...boosts }; + + if (boostsRemoved.length > 0) { + updatedBoosts[name] = boostsRemoved; + } else { + delete updatedBoosts[name]; + } + + actions.setSearchSettings({ + ...searchSettings, + boosts: updatedBoosts, + }); + } + }, + updateBoostFactor: ({ name, index, factor }) => { + const { searchSettings } = values; + const { boosts } = searchSettings; + const updatedBoosts = cloneDeep(boosts[name]); + updatedBoosts[index].factor = Math.round(factor * 10) / 10; + + actions.setSearchSettings({ + ...searchSettings, + boosts: { + ...boosts, + [name]: updatedBoosts, + }, + }); + }, + updateBoostValue: ({ name, boostIndex, valueIndex, value }) => { + const { searchSettings } = values; + const { boosts } = searchSettings; + const updatedBoosts: Boost[] = cloneDeep(boosts[name]); + const existingValue = updatedBoosts[boostIndex].value; + if (existingValue === undefined) { + updatedBoosts[boostIndex].value = [value]; + } else { + existingValue[valueIndex] = value; + } + + actions.setSearchSettings({ + ...searchSettings, + boosts: { + ...boosts, + [name]: updatedBoosts, + }, + }); + }, + updateBoostCenter: ({ name, boostIndex, value }) => { + const { searchSettings } = values; + const { boosts } = searchSettings; + const updatedBoosts = cloneDeep(boosts[name]); + const fieldType = values.schema[name]; + updatedBoosts[boostIndex].center = parseBoostCenter(fieldType, value); + + actions.setSearchSettings({ + ...searchSettings, + boosts: { + ...boosts, + [name]: updatedBoosts, + }, + }); + }, + addBoostValue: ({ name, boostIndex }) => { + const { searchSettings } = values; + const { boosts } = searchSettings; + const updatedBoosts = cloneDeep(boosts[name]); + const updatedBoost = updatedBoosts[boostIndex]; + if (updatedBoost) { + updatedBoost.value = Array.isArray(updatedBoost.value) ? updatedBoost.value : ['']; + updatedBoost.value.push(''); + } + + actions.setSearchSettings({ + ...searchSettings, + boosts: { + ...boosts, + [name]: updatedBoosts, + }, + }); + }, + removeBoostValue: ({ name, boostIndex, valueIndex }) => { + const { searchSettings } = values; + const { boosts } = searchSettings; + const updatedBoosts = cloneDeep(boosts[name]); + const boostValue = updatedBoosts[boostIndex].value; + + if (boostValue === undefined) return; + + boostValue.splice(valueIndex, 1); + actions.setSearchSettings({ + ...searchSettings, + boosts: { + ...boosts, + [name]: updatedBoosts, + }, + }); + }, + updateBoostSelectOption: ({ name, boostIndex, optionType, value }) => { + const { searchSettings } = values; + const { boosts } = searchSettings; + const updatedBoosts = cloneDeep(boosts[name]); + updatedBoosts[boostIndex][optionType] = value; + + actions.setSearchSettings({ + ...searchSettings, + boosts: { + ...boosts, + [name]: updatedBoosts, + }, + }); + }, + updateSearchValue: (query) => { + actions.setSearchQuery(query); + actions.getSearchResults(); + }, + }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts index 25187df89d64..a1ed9797b9f5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts @@ -7,17 +7,31 @@ export type BoostType = 'value' | 'functional' | 'proximity'; -export interface Boost { - type: BoostType; +export interface BaseBoost { operation?: string; function?: string; +} + +// A boost that comes from the server, before we normalize it has a much looser schema +export interface RawBoost extends BaseBoost { + type: BoostType; newBoost?: boolean; center?: string | number; - value?: string | number | string[] | number[]; + value?: string | number | boolean | object | Array; factor: number; } +// We normalize raw boosts to make them safer and easier to work with +export interface Boost extends RawBoost { + value?: string[]; +} export interface SearchSettings { boosts: Record; - search_fields: object; + search_fields: Record< + string, + { + weight: number; + } + >; + result_fields?: object; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts new file mode 100644 index 000000000000..a6598bf991c1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts @@ -0,0 +1,147 @@ +/* + * 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 { BoostType } from './types'; +import { + filterIfTerm, + normalizeBoostValues, + removeBoostStateProps, + parseBoostCenter, +} from './utils'; + +describe('filterIfTerm', () => { + it('will filter a list of strings to a list of strings containing the specified string', () => { + expect(filterIfTerm(['jalepeno', 'no', 'not', 'panorama', 'truck'], 'no')).toEqual([ + 'jalepeno', + 'no', + 'not', + 'panorama', + ]); + }); + + it('will not filter at all if an empty string is provided', () => { + expect(filterIfTerm(['jalepeno', 'no', 'not', 'panorama', 'truck'], '')).toEqual([ + 'jalepeno', + 'no', + 'not', + 'panorama', + 'truck', + ]); + }); +}); + +describe('removeBoostStateProps', () => { + it('will remove the newBoost flag from boosts within the provided searchSettings object', () => { + const searchSettings = { + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + newBoost: true, + }, + ], + }, + search_fields: { + foo: { + weight: 1, + }, + }, + }; + expect(removeBoostStateProps(searchSettings)).toEqual({ + ...searchSettings, + boosts: { + foo: [ + { + type: 'value' as BoostType, + factor: 5, + }, + ], + }, + }); + }); +}); + +describe('parseBoostCenter', () => { + it('should parse a boost center', () => { + expect(parseBoostCenter('text', 5)).toEqual(5); + expect(parseBoostCenter('text', '4')).toEqual('4'); + expect(parseBoostCenter('number', 5)).toEqual(5); + expect(parseBoostCenter('number', '5')).toEqual(5); + }); +}); + +describe('normalizeBoostValues', () => { + const boosts = { + foo: [ + { + type: 'value' as BoostType, + factor: 9.5, + value: 1, + }, + { + type: 'value' as BoostType, + factor: 9.5, + value: '1', + }, + { + type: 'value' as BoostType, + factor: 9.5, + value: [1], + }, + { + type: 'value' as BoostType, + factor: 9.5, + value: ['1'], + }, + { + type: 'value' as BoostType, + factor: 9.5, + value: [ + '1', + 1, + '2', + 2, + true, + { + b: 'a', + }, + {}, + ], + }, + ], + bar: [ + { + type: 'proximity' as BoostType, + factor: 9.5, + }, + ], + sp_def: [ + { + type: 'functional' as BoostType, + factor: 5, + }, + ], + }; + + it('converts all value types to string for consistency', () => { + expect(normalizeBoostValues(boosts)).toEqual({ + bar: [{ factor: 9.5, type: 'proximity' }], + foo: [ + { factor: 9.5, type: 'value', value: ['1'] }, + { factor: 9.5, type: 'value', value: ['1'] }, + { factor: 9.5, type: 'value', value: ['1'] }, + { factor: 9.5, type: 'value', value: ['1'] }, + { + factor: 9.5, + type: 'value', + value: ['1', '1', '2', '2', 'true', '[object Object]', '[object Object]'], + }, + ], + sp_def: [{ type: 'functional', factor: 5 }], + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.ts new file mode 100644 index 000000000000..e2fd0f0bbd65 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cloneDeep, omit } from 'lodash'; + +import { NUMBER } from '../../../shared/constants/field_types'; +import { SchemaTypes } from '../../../shared/types'; + +import { RawBoost, Boost, SearchSettings, BoostType } from './types'; + +// If the user hasn't entered a filter, then we can skip filtering the array entirely +export const filterIfTerm = (array: string[], filterTerm: string): string[] => { + return filterTerm === '' ? array : array.filter((item) => item.includes(filterTerm)); +}; + +export const removeBoostStateProps = (searchSettings: SearchSettings) => { + const updatedSettings = cloneDeep(searchSettings); + const { boosts } = updatedSettings; + const keys = Object.keys(boosts); + keys.forEach((key) => boosts[key].forEach((boost) => delete boost.newBoost)); + + return updatedSettings; +}; + +export const parseBoostCenter = (fieldType: SchemaTypes, value: string | number) => { + // Leave non-numeric fields alone + if (fieldType === NUMBER) { + const floatValue = parseFloat(value as string); + return isNaN(floatValue) ? value : floatValue; + } + return value; +}; + +const toArray = (v: T | T[]): T[] => (Array.isArray(v) ? v : [v]); +const toString = (v1: T) => String(v1); + +const normalizeBoostValue = (boost: RawBoost): Boost => { + if (!boost.hasOwnProperty('value')) { + // Can't simply do `return boost` here as TS can't infer the correct type + return omit(boost, 'value'); + } + + return { + ...boost, + type: boost.type as BoostType, + value: toArray(boost.value).map(toString), + }; +}; + +// Data may have been set to invalid types directly via the public App Search API. Since these are invalid, we don't want to deal +// with them as valid types in the UI. For that reason, we stringify all values here, as the data comes in. +// Additionally, values can be in single values or in arrays. +export const normalizeBoostValues = (boosts: Record): Record => + Object.entries(boosts).reduce((newBoosts, [fieldName, boostList]) => { + return { + ...newBoosts, + [fieldName]: boostList.map(normalizeBoostValue), + }; + }, {}); From b18e4ec9b8638113e27c2974fd20af2c6df8b7b7 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Wed, 10 Feb 2021 13:05:33 -0800 Subject: [PATCH 27/32] [DOCS] Uses variable to refer to query profiler (#90976) --- docs/dev-tools/searchprofiler/getting-started.asciidoc | 2 +- docs/user/dev-tools.asciidoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev-tools/searchprofiler/getting-started.asciidoc b/docs/dev-tools/searchprofiler/getting-started.asciidoc index 7cd54db5562b..ad73d03bcbfd 100644 --- a/docs/dev-tools/searchprofiler/getting-started.asciidoc +++ b/docs/dev-tools/searchprofiler/getting-started.asciidoc @@ -2,7 +2,7 @@ [[profiler-getting-started]] === Getting Started -The {searchprofiler} is automatically enabled in {kib}. Open the main menu, click *Dev Tools*, then click *Search Profiler* +The {searchprofiler} is automatically enabled in {kib}. Open the main menu, click *Dev Tools*, then click *{searchprofiler}* to get started. {searchprofiler} displays the names of the indices searched, the shards in each index, diff --git a/docs/user/dev-tools.asciidoc b/docs/user/dev-tools.asciidoc index 0ee7fbc741e0..0c5bef489dd0 100644 --- a/docs/user/dev-tools.asciidoc +++ b/docs/user/dev-tools.asciidoc @@ -15,7 +15,7 @@ a| <> | Interact with the REST API of Elasticsearch, including sending requests and viewing API documentation. -a| <> +a| <> | Inspect and analyze your search queries. From 4cd0548f482876e2a0822926d985137d59a8f7e8 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Wed, 10 Feb 2021 16:42:00 -0500 Subject: [PATCH 28/32] [CI] Fix auto-backport condditions so that it doesn't trigger for other labels (#91042) --- .github/workflows/backport.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index f64b9e95fbaa..238a21161b12 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -9,9 +9,14 @@ on: jobs: backport: name: Backport PR - if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'auto-backport') + if: | + github.event.pull_request.merged == true + && contains(github.event.pull_request.labels.*.name, 'auto-backport') + && ( + (github.event.action == 'labeled' && github.event.label.name == 'auto-backport') + || (github.event.action == 'closed') + ) runs-on: ubuntu-latest - steps: - name: 'Get backport config' run: | From c92af5a4d5bf3bab6b1dbbf5eb7250111b001aa3 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Wed, 10 Feb 2021 17:04:01 -0500 Subject: [PATCH 29/32] [Fleet] Restrict integration changes for managed policies (#90675) ## Summary - [x] Integrations cannot be added ~~, unless with a force flag~~ - [x] API - [x] UI - [x] tests - [x] Integrations cannot be removed ~~, unless with a force flag~~ - [x] API - [x] UI - [x] tests closes https://github.com/elastic/kibana/issues/90445 refs https://github.com/elastic/kibana/issues/89617 ### Cannot add integrations to managed policy Screen Shot 2021-02-08 at 1 56 32 PM ### Cannot delete integrations from managed policy Screen Shot 2021-02-08 at 3 05 16 PM ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../server/routes/package_policy/handlers.ts | 5 +- .../fleet/server/services/agent_policy.ts | 14 +- .../fleet/server/services/package_policy.ts | 30 +++-- .../test/fleet_api_integration/apis/index.js | 1 + .../apis/package_policy/create.ts | 48 ++++++- .../apis/package_policy/delete.ts | 127 ++++++++++++++++++ .../apis/package_policy/update.ts | 48 ++++++- 7 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 x-pack/test/fleet_api_integration/apis/package_policy/delete.ts diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index ef0c34ee5639..6b35f74b3feb 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -79,11 +79,10 @@ export const createPackagePolicyHandler: RequestHandler< const esClient = context.core.elasticsearch.client.asCurrentUser; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; - let newData = { ...request.body }; try { - newData = await packagePolicyService.runExternalCallbacks( + const newData = await packagePolicyService.runExternalCallbacks( 'packagePolicyCreate', - newData, + { ...request.body }, context, request ); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 9800ddf95f7b..31e9a63175d1 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -36,7 +36,11 @@ import { FleetServerPolicy, AGENT_POLICY_INDEX, } from '../../common'; -import { AgentPolicyNameExistsError, AgentPolicyDeletionError } from '../errors'; +import { + AgentPolicyNameExistsError, + AgentPolicyDeletionError, + IngestManagerError, +} from '../errors'; import { createAgentPolicyAction, listAgents } from './agents'; import { packagePolicyService } from './package_policy'; import { outputService } from './output'; @@ -382,6 +386,10 @@ class AgentPolicyService { throw new Error('Agent policy not found'); } + if (oldAgentPolicy.is_managed) { + throw new IngestManagerError(`Cannot update integrations of managed policy ${id}`); + } + return await this._update( soClient, esClient, @@ -409,6 +417,10 @@ class AgentPolicyService { throw new Error('Agent policy not found'); } + if (oldAgentPolicy.is_managed) { + throw new IngestManagerError(`Cannot remove integrations of managed policy ${id}`); + } + return await this._update( soClient, esClient, diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 4b04014b2096..8d1ac90f3ec1 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -25,6 +25,7 @@ import { doesAgentPolicyAlreadyIncludePackage, } from '../../common'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../constants'; +import { IngestManagerError, ingestErrorToResponseOptions } from '../errors'; import { NewPackagePolicy, UpdatePackagePolicy, @@ -63,15 +64,20 @@ class PackagePolicyService { const parentAgentPolicy = await agentPolicyService.get(soClient, packagePolicy.policy_id); if (!parentAgentPolicy) { throw new Error('Agent policy not found'); - } else { - if ( - (parentAgentPolicy.package_policies as PackagePolicy[]).find( - (siblingPackagePolicy) => siblingPackagePolicy.name === packagePolicy.name - ) - ) { - throw new Error('There is already a package with the same name on this agent policy'); - } } + if (parentAgentPolicy.is_managed) { + throw new IngestManagerError( + `Cannot add integrations to managed policy ${parentAgentPolicy.id}` + ); + } + if ( + (parentAgentPolicy.package_policies as PackagePolicy[]).find( + (siblingPackagePolicy) => siblingPackagePolicy.name === packagePolicy.name + ) + ) { + throw new Error('There is already a package with the same name on this agent policy'); + } + // Add ids to stream const packagePolicyId = options?.id || uuid.v4(); let inputs: PackagePolicyInput[] = packagePolicy.inputs.map((input) => @@ -285,6 +291,9 @@ class PackagePolicyService { if (!parentAgentPolicy) { throw new Error('Agent policy not found'); } else { + if (parentAgentPolicy.is_managed) { + throw new IngestManagerError(`Cannot update integrations of managed policy ${id}`); + } if ( (parentAgentPolicy.package_policies as PackagePolicy[]).find( (siblingPackagePolicy) => @@ -295,7 +304,7 @@ class PackagePolicyService { } } - let inputs = await restOfPackagePolicy.inputs.map((input) => + let inputs = restOfPackagePolicy.inputs.map((input) => assignStreamIdToInput(oldPackagePolicy.id, input) ); @@ -363,10 +372,11 @@ class PackagePolicyService { name: packagePolicy.name, success: true, }); - } catch (e) { + } catch (error) { result.push({ id, success: false, + ...ingestErrorToResponseOptions(error), }); } } diff --git a/x-pack/test/fleet_api_integration/apis/index.js b/x-pack/test/fleet_api_integration/apis/index.js index 8c66db9c418e..44431795a34b 100644 --- a/x-pack/test/fleet_api_integration/apis/index.js +++ b/x-pack/test/fleet_api_integration/apis/index.js @@ -37,6 +37,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./package_policy/create')); loadTestFile(require.resolve('./package_policy/update')); loadTestFile(require.resolve('./package_policy/get')); + loadTestFile(require.resolve('./package_policy/delete')); // Agent policies loadTestFile(require.resolve('./agent_policy/index')); diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts index 8e339bc78b08..c9c871e280f1 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { warnAndSkipTest } from '../../helpers'; @@ -39,6 +39,52 @@ export default function ({ getService }: FtrProviderContext) { .send({ agentPolicyId }); }); + it('should fail for managed agent policies', async function () { + if (server.enabled) { + // get a managed policy + const { + body: { item: managedPolicy }, + } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: `Managed policy from ${Date.now()}`, + namespace: 'default', + is_managed: true, + }); + + // try to add an integration to the managed policy + const { body } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: 'default', + policy_id: managedPolicy.id, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(400); + + expect(body.statusCode).to.be(400); + expect(body.message).to.contain('Cannot add integrations to managed policy'); + + // delete policy we just made + await supertest.post(`/api/fleet/agent_policies/delete`).set('kbn-xsrf', 'xxxx').send({ + agentPolicyId: managedPolicy.id, + }); + } else { + warnAndSkipTest(this, log); + } + }); + it('should work with valid values', async function () { if (server.enabled) { await supertest diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts b/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts new file mode 100644 index 000000000000..e64ba8580d14 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + + // use function () {} and not () => {} here + // because `this` has to point to the Mocha context + // see https://mochajs.org/#arrow-functions + + describe('Package Policy - delete', async function () { + skipIfNoDockerRegistry(providerContext); + let agentPolicy: any; + let packagePolicy: any; + + before(async function () { + let agentPolicyResponse = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test policy', + namespace: 'default', + is_managed: false, + }); + + // if one already exists, re-use that + if (agentPolicyResponse.body.statusCode === 409) { + const errorRegex = /^agent policy \'(?[\w,\-]+)\' already exists/i; + const result = errorRegex.exec(agentPolicyResponse.body.message); + if (result?.groups?.id) { + agentPolicyResponse = await supertest + .put(`/api/fleet/agent_policies/${result.groups.id}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test policy', + namespace: 'default', + is_managed: false, + }); + } + } + agentPolicy = agentPolicyResponse.body.item; + + const { body: packagePolicyResponse } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: 'default', + policy_id: agentPolicy.id, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }); + packagePolicy = packagePolicyResponse.item; + }); + + after(async function () { + await supertest + .post(`/api/fleet/agent_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ agentPolicyId: agentPolicy.id }); + + await supertest + .post(`/api/fleet/package_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ packagePolicyIds: [packagePolicy.id] }); + }); + + it('should fail on managed agent policies', async function () { + // update existing policy to managed + await supertest + .put(`/api/fleet/agent_policies/${agentPolicy.id}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: agentPolicy.name, + namespace: agentPolicy.namespace, + is_managed: true, + }) + .expect(200); + + // try to delete + const { body: results } = await supertest + .post(`/api/fleet/package_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ packagePolicyIds: [packagePolicy.id] }) + .expect(200); + + // delete always succeeds (returns 200) with Array<{success: boolean}> + expect(Array.isArray(results)); + expect(results.length).to.be(1); + expect(results[0].success).to.be(false); + expect(results[0].body.message).to.contain('Cannot remove integrations of managed policy'); + + // revert existing policy to unmanaged + await supertest + .put(`/api/fleet/agent_policies/${agentPolicy.id}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: agentPolicy.name, + namespace: agentPolicy.namespace, + is_managed: false, + }) + .expect(200); + }); + + it('should work for unmanaged policies', async function () { + await supertest + .post(`/api/fleet/package_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ packagePolicyIds: [packagePolicy.id] }); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/update.ts b/x-pack/test/fleet_api_integration/apis/package_policy/update.ts index e0dc1a5d96b4..9a70c6ad004d 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/update.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/update.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; @@ -21,6 +21,7 @@ export default function (providerContext: FtrProviderContext) { describe('Package Policy - update', async function () { skipIfNoDockerRegistry(providerContext); let agentPolicyId: string; + let managedAgentPolicyId: string; let packagePolicyId: string; let packagePolicyId2: string; @@ -35,8 +36,30 @@ export default function (providerContext: FtrProviderContext) { name: 'Test policy', namespace: 'default', }); + agentPolicyId = agentPolicyResponse.item.id; + const { body: managedAgentPolicyResponse } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test managed policy', + namespace: 'default', + is_managed: true, + }); + + // if one already exists, re-use that + const managedExists = managedAgentPolicyResponse.statusCode === 409; + if (managedExists) { + const errorRegex = /^agent policy \'(?[\w,\-]+)\' already exists/i; + const result = errorRegex.exec(managedAgentPolicyResponse.message); + if (result?.groups?.id) { + managedAgentPolicyId = result.groups.id; + } + } else { + managedAgentPolicyId = managedAgentPolicyResponse.item.id; + } + const { body: packagePolicyResponse } = await supertest .post(`/api/fleet/package_policies`) .set('kbn-xsrf', 'xxxx') @@ -83,6 +106,29 @@ export default function (providerContext: FtrProviderContext) { .send({ agentPolicyId }); }); + it('should fail on managed agent policies', async function () { + const { body } = await supertest + .put(`/api/fleet/package_policies/${packagePolicyId}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: 'updated_namespace', + policy_id: managedAgentPolicyId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(400); + + expect(body.message).to.contain('Cannot update integrations of managed policy'); + }); + it('should work with valid values', async function () { await supertest .put(`/api/fleet/package_policies/${packagePolicyId}`) From 591bcc1c7135c744fd1d8e26dff064cf610b37f5 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 10 Feb 2021 14:13:49 -0800 Subject: [PATCH 30/32] [jest/ci] remove max-old-space-size override to use 4gb default (#91020) Co-authored-by: spalger --- test/scripts/test/jest_unit.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh index 1442a0f72872..fd1166b07f32 100755 --- a/test/scripts/test/jest_unit.sh +++ b/test/scripts/test/jest_unit.sh @@ -2,7 +2,5 @@ source src/dev/ci_setup/setup_env.sh -export NODE_OPTIONS="--max-old-space-size=2048" - checks-reporter-with-killswitch "Jest Unit Tests" \ node scripts/jest --ci --verbose --maxWorkers=8 From 9fe8ccce477afbeb540e92b72dd72a02ea0a688d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 10 Feb 2021 22:40:21 +0000 Subject: [PATCH 31/32] chore(NA): move the instruction to remove yarn global bazelisk package into the first place on install bazel tools (#91026) --- packages/kbn-pm/dist/index.js | 8 ++++---- packages/kbn-pm/src/utils/bazel/install_tools.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index d939e7b3000f..375ad634cbc1 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -48147,13 +48147,13 @@ async function installBazelTools(repoRootPath) { const bazeliskVersion = await readBazelToolsVersionFile(repoRootPath, '.bazeliskversion'); const bazelVersion = await readBazelToolsVersionFile(repoRootPath, '.bazelversion'); // Check what globals are installed - _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] verify if bazelisk is installed`); // Test if bazelisk is already installed in the correct version + _log__WEBPACK_IMPORTED_MODULE_4__["log"].debug(`[bazel_tools] verify if bazelisk is installed`); // Check if we need to remove bazelisk from yarn - const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); // Test if bazel bin is available + await tryRemoveBazeliskFromYarnGlobal(); // Test if bazelisk is already installed in the correct version - const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Check if we need to remove bazelisk from yarn + const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); // Test if bazel bin is available - await tryRemoveBazeliskFromYarnGlobal(); // Install bazelisk if not installed + const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); // Install bazelisk if not installed if (!isBazeliskPkgInstalled || !isBazelBinAlreadyAvailable) { _log__WEBPACK_IMPORTED_MODULE_4__["log"].info(`[bazel_tools] installing Bazel tools`); diff --git a/packages/kbn-pm/src/utils/bazel/install_tools.ts b/packages/kbn-pm/src/utils/bazel/install_tools.ts index b547c2bc141b..93acbe09b4ea 100644 --- a/packages/kbn-pm/src/utils/bazel/install_tools.ts +++ b/packages/kbn-pm/src/utils/bazel/install_tools.ts @@ -83,15 +83,15 @@ export async function installBazelTools(repoRootPath: string) { // Check what globals are installed log.debug(`[bazel_tools] verify if bazelisk is installed`); + // Check if we need to remove bazelisk from yarn + await tryRemoveBazeliskFromYarnGlobal(); + // Test if bazelisk is already installed in the correct version const isBazeliskPkgInstalled = await isBazeliskInstalled(bazeliskVersion); // Test if bazel bin is available const isBazelBinAlreadyAvailable = await isBazelBinAvailable(); - // Check if we need to remove bazelisk from yarn - await tryRemoveBazeliskFromYarnGlobal(); - // Install bazelisk if not installed if (!isBazeliskPkgInstalled || !isBazelBinAlreadyAvailable) { log.info(`[bazel_tools] installing Bazel tools`); From 8c4af6fc5eab965b614b09306cb0f7316ca4847e Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Wed, 10 Feb 2021 15:32:29 -0800 Subject: [PATCH 32/32] Removing the code plugin entirely for 8.0 (#77940) * Removing the code app entirely for 8.0 * Updating plugin list docs * Using a test plugin for the code_coverage integration tests * Fix borked test. Co-authored-by: Elastic Machine Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Tre' Seymour --- docs/developer/plugin-list.asciidoc | 4 -- .../fixtures/test_plugin/kibana.json | 6 +++ .../fixtures/test_plugin/server/index.ts | 13 +++++++ .../fixtures/test_plugin/server/plugin.ts | 23 +++++++++++ .../integration_tests/mocks/CODEOWNERS | 2 +- .../integration_tests/team_assignment.test.js | 7 +--- x-pack/plugins/code/kibana.json | 8 ---- x-pack/plugins/code/server/config.ts | 14 ------- x-pack/plugins/code/server/index.ts | 14 ------- x-pack/plugins/code/server/plugin.test.ts | 39 ------------------- x-pack/plugins/code/server/plugin.ts | 34 ---------------- 11 files changed, 45 insertions(+), 119 deletions(-) create mode 100644 src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/kibana.json create mode 100644 src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/index.ts create mode 100644 src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/plugin.ts delete mode 100644 x-pack/plugins/code/kibana.json delete mode 100644 x-pack/plugins/code/server/config.ts delete mode 100644 x-pack/plugins/code/server/index.ts delete mode 100644 x-pack/plugins/code/server/plugin.test.ts delete mode 100644 x-pack/plugins/code/server/plugin.ts diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 263addc98ee6..613f2d0fbf20 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -318,10 +318,6 @@ Failure to have auth enabled in Kibana will make for a broken UI. UI-based error |The cloud plugin adds cloud specific features to Kibana. -|{kib-repo}blob/{branch}/x-pack/plugins/code[code] -|WARNING: Missing README. - - |{kib-repo}blob/{branch}/x-pack/plugins/console_extensions/README.md[consoleExtensions] |This plugin provides autocomplete definitions of licensed APIs to the OSS Console plugin. diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/kibana.json b/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/kibana.json new file mode 100644 index 000000000000..cbb214b57570 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "codeCoverageTestPlugin", + "version": "kibana", + "server": true, + "ui": false +} diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/index.ts b/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/index.ts new file mode 100644 index 000000000000..5499a33fbf73 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/index.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 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 { Plugin } from './plugin'; + +export function plugin() { + return new Plugin(); +} diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/plugin.ts b/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/plugin.ts new file mode 100644 index 000000000000..d4704ba05b59 --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/plugin.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { CoreSetup } from 'kibana/server'; + +export class Plugin { + constructor() {} + + public setup(core: CoreSetup) {} + + public start() { + // called after all plugins are set up + } + + public stop() { + // called when plugin is torn down during Kibana's shutdown sequence + } +} diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/CODEOWNERS b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/CODEOWNERS index 1822c3fd95e3..77b220282035 100644 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/CODEOWNERS +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/mocks/CODEOWNERS @@ -3,4 +3,4 @@ # For more info, see https://help.github.com/articles/about-codeowners/ # App -/x-pack/plugins/code/ @elastic/kibana-tre +/src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin @elastic/kibana-tre diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js index 8fc34d29103b..8fe61ed76a92 100644 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/team_assignment.test.js @@ -39,11 +39,8 @@ describe('Team Assignment', () => { const { stdout } = await execa('grep', ['tre', teamAssignmentsPath], { cwd: ROOT_DIR }); const lines = stdout.split('\n').filter((line) => !line.includes('/target')); expect(lines).toEqual([ - 'x-pack/plugins/code/jest.config.js kibana-tre', - 'x-pack/plugins/code/server/config.ts kibana-tre', - 'x-pack/plugins/code/server/index.ts kibana-tre', - 'x-pack/plugins/code/server/plugin.test.ts kibana-tre', - 'x-pack/plugins/code/server/plugin.ts kibana-tre', + 'src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/index.ts kibana-tre', + 'src/dev/code_coverage/ingest_coverage/integration_tests/fixtures/test_plugin/server/plugin.ts kibana-tre', ]); }); }); diff --git a/x-pack/plugins/code/kibana.json b/x-pack/plugins/code/kibana.json deleted file mode 100644 index 815bc147d3cf..000000000000 --- a/x-pack/plugins/code/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "code", - "version": "8.0.0", - "kibanaVersion": "kibana", - "configPath": ["xpack", "code"], - "server": true, - "ui": false -} diff --git a/x-pack/plugins/code/server/config.ts b/x-pack/plugins/code/server/config.ts deleted file mode 100644 index 2cc3e78c0b96..000000000000 --- a/x-pack/plugins/code/server/config.ts +++ /dev/null @@ -1,14 +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 { schema } from '@kbn/config-schema'; - -const createCodeConfigSchema = () => { - return schema.any({ defaultValue: {} }); -}; - -export const CodeConfigSchema = createCodeConfigSchema(); diff --git a/x-pack/plugins/code/server/index.ts b/x-pack/plugins/code/server/index.ts deleted file mode 100644 index ccea83ca1ff9..000000000000 --- a/x-pack/plugins/code/server/index.ts +++ /dev/null @@ -1,14 +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 { PluginInitializerContext } from 'src/core/server'; -import { CodeConfigSchema } from './config'; -import { CodePlugin } from './plugin'; - -export const config = { schema: CodeConfigSchema }; -export const plugin = (initializerContext: PluginInitializerContext) => - new CodePlugin(initializerContext); diff --git a/x-pack/plugins/code/server/plugin.test.ts b/x-pack/plugins/code/server/plugin.test.ts deleted file mode 100644 index 512658ca4da8..000000000000 --- a/x-pack/plugins/code/server/plugin.test.ts +++ /dev/null @@ -1,39 +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 { coreMock } from '../../../../src/core/server/mocks'; - -import { CodePlugin } from './plugin'; - -describe('Code Plugin', () => { - describe('setup()', () => { - it('does not log deprecation message if no xpack.code.* configurations are set', async () => { - const context = coreMock.createPluginInitializerContext(); - const plugin = new CodePlugin(context); - - await plugin.setup(); - - expect(context.logger.get).not.toHaveBeenCalled(); - }); - - it('logs deprecation message if any xpack.code.* configurations are set', async () => { - const context = coreMock.createPluginInitializerContext({ - foo: 'bar', - }); - const warn = jest.fn(); - context.logger.get = jest.fn().mockReturnValue({ warn }); - const plugin = new CodePlugin(context); - - await plugin.setup(); - - expect(context.logger.get).toHaveBeenCalledWith('config', 'deprecation'); - expect(warn.mock.calls[0][0]).toMatchInlineSnapshot( - `"The experimental app \\"Code\\" has been removed from Kibana. Remove all xpack.code.* configurations from kibana.yml so Kibana does not fail to start up in the next major version."` - ); - }); - }); -}); diff --git a/x-pack/plugins/code/server/plugin.ts b/x-pack/plugins/code/server/plugin.ts deleted file mode 100644 index eb7481d12387..000000000000 --- a/x-pack/plugins/code/server/plugin.ts +++ /dev/null @@ -1,34 +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 { TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext, Plugin } from 'src/core/server'; -import { CodeConfigSchema } from './config'; - -/** - * Represents Code Plugin instance that will be managed by the Kibana plugin system. - */ -export class CodePlugin implements Plugin { - constructor(private readonly initializerContext: PluginInitializerContext) {} - - public async setup() { - const config = this.initializerContext.config.get>(); - - if (config && Object.keys(config).length > 0) { - this.initializerContext.logger - .get('config', 'deprecation') - .warn( - 'The experimental app "Code" has been removed from Kibana. Remove all xpack.code.* ' + - 'configurations from kibana.yml so Kibana does not fail to start up in the next major version.' - ); - } - } - - public start() {} - - public stop() {} -}